diff --git a/.codecov.yml b/.codecov.yml index f9c5935a..a04e2e31 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,5 +1,4 @@ ignore: - - "src/win_bindings.rs" # Do not notify until at least three results have been uploaded from the CI pipeline. # (This corresponds to the three main platforms we support: Linux, macOS, and Windows.) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 81fddf06..3b997179 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -18,16 +18,34 @@ concurrency: permissions: contents: read +defaults: + run: + shell: bash + jobs: + toolchains: + runs-on: ubuntu-latest + outputs: + toolchains: ${{ steps.toolchains.outputs.toolchains }} + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + with: + sparse-checkout: Cargo.toml + - id: toolchains + run: | + msrv="$(grep rust-version Cargo.toml | tr -d '"' | cut -f3 -d\ )" + echo "toolchains=[\"$msrv\", \"stable\", \"nightly\"]" >> "$GITHUB_OUTPUT" + check: + needs: toolchains strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - # Keep low end in sync with Cargo.toml - rust-toolchain: [1.76.0, stable, nightly] + rust-toolchain: ${{ fromJSON(needs.toolchains.outputs.toolchains) }} type: [debug] include: + # Also do some release builds on the latest OS versions. - os: ubuntu-latest rust-toolchain: stable type: release @@ -37,6 +55,7 @@ jobs: - os: windows-latest rust-toolchain: stable type: release + # Also do some debug builds on the oldest OS versions. - os: ubuntu-20.04 rust-toolchain: stable type: debug @@ -49,9 +68,6 @@ jobs: env: BUILD_TYPE: ${{ matrix.type == 'release' && '--release' || '' }} runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -65,30 +81,37 @@ jobs: - uses: ./neqo/.github/actions/rust with: version: ${{ matrix.rust-toolchain }} - components: ${{ matrix.rust-toolchain == 'stable' && 'llvm-tools-preview' || '' }} ${{ matrix.rust-toolchain == 'nightly' && 'rust-src' || '' }} + components: ${{ matrix.rust-toolchain == 'stable' && 'llvm-tools' || '' }} ${{ matrix.rust-toolchain == 'nightly' && 'rust-src' || '' }} tools: ${{ matrix.rust-toolchain == 'stable' && 'cargo-llvm-cov, ' || '' }} token: ${{ secrets.GITHUB_TOKEN }} - name: Check run: | - # shellcheck disable=SC2086 - cargo +${{ matrix.rust-toolchain }} check $BUILD_TYPE --all-targets + OPTIONS=(--all-targets) + if [ "$BUILD_TYPE" ]; then + OPTIONS+=("$BUILD_TYPE") + fi + cargo +${{ matrix.rust-toolchain }} check "${OPTIONS[@]}" - name: Run tests and determine coverage env: RUST_LOG: trace run: | - # shellcheck disable=SC2086 + OPTIONS=(--no-fail-fast) + if [ "$BUILD_TYPE" ]; then + OPTIONS+=("$BUILD_TYPE") + fi if [ "${{ matrix.rust-toolchain }}" == "stable" ] && [ "${{ matrix.type }}" == "debug" ] && [ "${{endsWith(matrix.os, '-latest') && 'latest' || '' }}" == "latest" ]; then - cargo +${{ matrix.rust-toolchain }} llvm-cov test $BUILD_TYPE --no-fail-fast --lcov --output-path lcov.info + cargo +${{ matrix.rust-toolchain }} llvm-cov test "${OPTIONS[@]}" --lcov --output-path lcov.info else if [ "${{ startsWith(matrix.os, 'windows') && 'windows' || '' }}" == "windows" ]; then # The codegen_windows_bindings test only succeeds when run via llvm-cov?! - export FILTER="-- --skip codegen_windows_bindings" + OPTIONS+=(-- --skip codegen_windows_bindings) fi - cargo +${{ matrix.rust-toolchain }} test $BUILD_TYPE --no-fail-fast $FILTER + cargo +${{ matrix.rust-toolchain }} test "${OPTIONS[@]}" fi cargo +${{ matrix.rust-toolchain }} bench --no-run + - uses: codecov/codecov-action@5c47607acb93fed5485fdbf7232e8a31425f672a # v5.0.2 with: files: lcov.info @@ -103,18 +126,30 @@ jobs: if: (matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest') && matrix.rust-toolchain == 'nightly' env: RUST_LOG: trace + ASAN_OPTIONS: detect_leaks=1:detect_stack_use_after_return=1 run: | if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + sudo apt-get install -y --no-install-recommends llvm TARGET="x86_64-unknown-linux-gnu" SANITIZERS="address thread leak memory" elif [ "${{ matrix.os }}" = "macos-latest" ]; then + # llvm-symbolizer (as part of llvm) is installed by default on macOS runners TARGET="aarch64-apple-darwin" # no memory and leak sanitizer support yet SANITIZERS="address thread" + # Suppress non-mtu leaks on macOS. TODO: Check occasionally if these are still needed. + { + echo "leak:dyld4::RuntimeState" + echo "leak:fetchInitializingClassList" + } > suppressions.txt + # shellcheck disable=SC2155 + export LSAN_OPTIONS="suppressions=$(pwd)/suppressions.txt" fi for sanitizer in $SANITIZERS; do echo "Running tests with $sanitizer sanitizer..." - RUSTFLAGS="-Z sanitizer=$sanitizer" RUSTDOCFLAGS="-Z sanitizer=$sanitizer" cargo +nightly test -Z build-std --target "$TARGET" + export RUSTFLAGS="-Z sanitizer=$sanitizer" + export RUSTDOCFLAGS="$RUSTFLAGS" + cargo +nightly test -Z build-std --target "$TARGET" done clippy: @@ -123,9 +158,6 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -226,50 +258,82 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - run: curl -o rustup.sh --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs - if: matrix.os == 'freebsd' uses: vmactions/freebsd-vm@debf37ca7b7fa40e19c542ef7ba30d6054a706a4 with: usesh: true - copyback: false + envs: "CARGO_TERM_COLOR RUST_BACKTRACE GITHUB_ACTIONS" prepare: | - mkdir -p /usr/local/etc/pkg/repos - sed 's/quarterly/latest/' /etc/pkg/FreeBSD.conf > /usr/local/etc/pkg/repos/FreeBSD.conf - pkg update - pkg install -y rust + pkg install -y curl llvm run: | + sh rustup.sh --default-toolchain stable --component llvm-tools -y + . "$HOME/.cargo/env" + export RUST_LOG=trace + cargo install cargo-llvm-cov --locked cargo check --all-targets - RUST_LOG=trace cargo test --no-fail-fast + cargo clippy + cargo llvm-cov test --no-fail-fast --lcov --output-path lcov.info + cargo test --no-fail-fast --release - if: matrix.os == 'openbsd' uses: vmactions/openbsd-vm@0cfe06e734a0ea3a546fca7ebf200b984b94d58a with: usesh: true - copyback: false + envs: "CARGO_TERM_COLOR RUST_BACKTRACE GITHUB_ACTIONS" prepare: | - pkg_add rust + pkg_add rust llvm-16.0.6p30 # rustup doesn't support OpenBSD at all run: | + export LIBCLANG_PATH=/usr/local/llvm16/lib + export RUST_LOG=trace cargo check --all-targets - RUST_LOG=trace cargo test --no-fail-fast + cargo clippy + cargo test --no-fail-fast + cargo test --no-fail-fast --release - if: matrix.os == 'netbsd' uses: vmactions/netbsd-vm@7c9086fdb4cc1aa814cda6e305390c2b966551a9 with: usesh: true - copyback: false + envs: "CARGO_TERM_COLOR RUST_BACKTRACE GITHUB_ACTIONS" prepare: | - /usr/sbin/pkg_add rust + /usr/sbin/pkg_add pkgin + pkgin -y install curl clang run: | + sh rustup.sh --default-toolchain stable --component 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 - RUST_LOG=trace cargo test --no-fail-fast + 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 --no-fail-fast --lcov --output-path lcov.info || true + cargo test --no-fail-fast --release - if: matrix.os == 'solaris' uses: vmactions/solaris-vm@a89b9438868c70db27e41625f0a5de6ff5e90809 with: usesh: true - copyback: false - prepare: | - pkg install cargo + envs: "CARGO_TERM_COLOR RUST_BACKTRACE GITHUB_ACTIONS" run: | + sh rustup.sh --default-toolchain stable --component llvm-tools -y + . "$HOME/.cargo/env" + export RUST_LOG=trace + cargo install cargo-llvm-cov --locked cargo check --all-targets - RUST_LOG=trace cargo test --no-fail-fast + cargo clippy + cargo llvm-cov test --no-fail-fast --lcov --output-path lcov.info + cargo test --no-fail-fast --release + + - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + with: + file: lcov.info + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index 4e0a8dfd..c605b532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ edition = "2021" license = "MIT OR Apache-2.0" # Don't increase beyond what Firefox is currently using: # https://searchfox.org/mozilla-central/search?q=MINIMUM_RUST_VERSION&path=python/mozboot/mozboot/util.py -# Also keep in sync with .github/workflows/check.yml rust-version = "1.76.0" [badges] @@ -23,18 +22,22 @@ maintenance = { status = "actively-developed", branch = "main" } [dependencies] # Don't increase beyond what Firefox is currently using: https://searchfox.org/mozilla-central/source/Cargo.lock -libc = { version = "0.2", default-features = false } +libc = { version = ">=0.2.161", default-features = false } +static_assertions = { version = "1.1", default-features = false } -[dev-dependencies] -rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } - -[target."cfg(windows)".dependencies] +[target.'cfg(windows)'.dependencies] # Don't increase beyond what Firefox is currently using: https://searchfox.org/mozilla-central/source/Cargo.lock windows-core = "0.58" windows-targets = "0.52" -[target."cfg(windows)".dev-dependencies] -windows-bindgen = { version = "0.58" } # MSRV is 1.70 +[build-dependencies] +cfg_aliases = "0.2" + +[target.'cfg(not(windows))'.build-dependencies] +bindgen = { version = "0.69.5" } + +[target.'cfg(windows)'.build-dependencies] +windows-bindgen = { version = "0.58" } [lints.clippy] cargo = { level = "warn", priority = -1 } diff --git a/README.md b/README.md index 6359d423..760efb5f 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,15 @@ towards a given destination `SocketAddr`, optionally from a given local `SocketA ## Usage -This crate exports a single function `interface_and_mtu` that, given a pair of local and remote `SocketAddr`s, returns the name and [maximum transmission unit (MTU)](https://en.wikipedia.org/wiki/Maximum_transmission_unit) of the local network interface used by a socket bound to the local address and connected towards the remote destination. - -If the local address is `None`, the function will let the operating system choose the local -address based on the given remote address. If the remote address is `None`, the function will -return the name and MTU of the local network interface with the given local address. +This crate exports a single function `interface_and_mtu` that returns the name and +[maximum transmission unit (MTU)](https://en.wikipedia.org/wiki/Maximum_transmission_unit) +of the outgoing network interface towards a remote destination identified by an `IpAddr`. ## Example - ```rust -let saddr = "127.0.0.1:443".parse().unwrap(); -let (name, mtu) = mtu::interface_and_mtu(&(None, saddr)).unwrap(); -println!("MTU for {saddr:?} is {mtu} on {name}"); +let destination = IpAddr::V4(Ipv4Addr::LOCALHOST); +let (name, mtu): (String, usize) = mtu::interface_and_mtu(destination).unwrap(); +println!("MTU towards {destination} is {mtu} on {name}"); ``` ## Supported Platforms diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..a4843034 --- /dev/null +++ b/build.rs @@ -0,0 +1,95 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +pub const BINDINGS: &str = "bindings.rs"; + +#[cfg(not(windows))] +fn bindgen() { + #[cfg(target_os = "linux")] + let bindings = bindgen::Builder::default() + .header_contents("rtnetlink.h", "#include ") + // Only generate bindings for the following types + .allowlist_type("rtattr|rtmsg|ifinfomsg|nlmsghdr"); + #[cfg(not(target_os = "linux"))] + let bindings = bindgen::Builder::default() + .header_contents( + "route.h", + "#include \n#include \n#include ", + ) + // Only generate bindings for the following types + .allowlist_type("rt_msghdr|rt_metrics"); + let bindings = bindings + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + // Constants should be generated as &CStr instead of &[u8]. + .generate_cstr(true) + // Always emit explicit padding fields. + .explicit_padding(true) + // Default trait should be derived when possible + .derive_default(true) + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/$BINDINGS file. + let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()).join(BINDINGS); + bindings + .write_to_file(out_path.clone()) + .expect("Couldn't write bindings!"); + println!("cargo:rustc-env=BINDINGS={}", out_path.display()); +} + +#[cfg(windows)] +fn bindgen() { + let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()).join(BINDINGS); + windows_bindgen::bindgen([ + "--out", + out_path.to_str().unwrap(), + "--config", + "flatten", + "no-inner-attributes", + "minimal", + "--filter", + "Windows.Win32.Foundation.NO_ERROR", + "Windows.Win32.Networking.WinSock.AF_INET", + "Windows.Win32.Networking.WinSock.AF_INET6", + "Windows.Win32.Networking.WinSock.AF_UNSPEC", + "Windows.Win32.Networking.WinSock.SOCKADDR_INET", + "Windows.Win32.NetworkManagement.IpHelper.FreeMibTable", + "Windows.Win32.NetworkManagement.IpHelper.GetBestInterfaceEx", + "Windows.Win32.NetworkManagement.IpHelper.GetIpInterfaceTable", + "Windows.Win32.NetworkManagement.IpHelper.if_indextoname", + "Windows.Win32.NetworkManagement.IpHelper.MIB_IPINTERFACE_ROW", + "Windows.Win32.NetworkManagement.Ndis.IF_MAX_STRING_SIZE", + ]) + .expect("Couldn't write bindings!"); + println!("cargo:rustc-env=BINDINGS={}", out_path.display()); +} + +fn main() { + // Setup cfg aliases + cfg_aliases::cfg_aliases! { + // Platforms + apple: { + any( + target_os = "macos", + target_os = "ios", + target_os = "tvos", + target_os = "visionos" + ) + }, + bsd: { + any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" + ) + } + } + bindgen(); +} diff --git a/src/bsd.rs b/src/bsd.rs new file mode 100644 index 00000000..ebe48997 --- /dev/null +++ b/src/bsd.rs @@ -0,0 +1,305 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::{ + ffi::CStr, + io::{Error, ErrorKind, Read, Result, Write}, + marker::PhantomData, + mem::size_of, + net::IpAddr, + 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, +}; +use static_assertions::{const_assert, const_assert_eq}; + +#[allow( + non_camel_case_types, + clippy::struct_field_names, + clippy::too_many_lines +)] +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(apple)] +const ALIGN: usize = size_of::(); + +#[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] +// 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 +const ALIGN: usize = size_of::(); + +use crate::{aligned_by, default_err}; + +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); + +const_assert!(size_of::() + ALIGN <= u8::MAX as usize); +const_assert!(size_of::() + ALIGN <= u8::MAX as usize); +const_assert!(size_of::() <= u8::MAX as usize); + +struct IfAddrs(*mut ifaddrs); + +impl Default for IfAddrs { + fn default() -> Self { + Self(ptr::null_mut()) + } +} + +impl IfAddrs { + fn new() -> Result { + let mut ifap = Self::default(); + // getifaddrs allocates memory for the linked list of interfaces that is freed by + // `IfAddrs::drop`. + if unsafe { getifaddrs(ptr::from_mut(&mut ifap.0)) } != 0 { + return Err(Error::last_os_error()); + } + Ok(ifap) + } + + const fn iter(&self) -> IfAddrPtr { + IfAddrPtr { + ptr: self.0, + _ref: PhantomData, + } + } +} + +impl Drop for IfAddrs { + fn drop(&mut self) { + if !self.0.is_null() { + // Free the memory allocated by `getifaddrs`. + unsafe { freeifaddrs(self.0) }; + } + } +} + +struct IfAddrPtr<'a> { + ptr: *mut ifaddrs, + _ref: PhantomData<&'a ifaddrs>, +} + +impl IfAddrPtr<'_> { + fn addr(&self) -> libc::sockaddr { + unsafe { *self.ifa_addr } + } + + fn name(&self) -> String { + unsafe { CStr::from_ptr(self.ifa_name).to_string_lossy().to_string() } + } + + fn data(&self) -> Option { + if self.ifa_data.is_null() { + None + } else { + Some(unsafe { self.ifa_data.cast::().read() }) + } + } +} + +impl Deref for IfAddrPtr<'_> { + type Target = ifaddrs; + + fn deref(&self) -> &Self::Target { + unsafe { self.ptr.as_ref().unwrap() } + } +} + +impl Iterator for IfAddrPtr<'_> { + type Item = Self; + + fn next(&mut self) -> Option { + ptr::NonNull::new(self.ptr).map(|p| { + self.ptr = unsafe { p.as_ref().ifa_next }; + IfAddrPtr { + ptr: p.as_ptr(), + _ref: PhantomData, + } + }) + } +} + +fn if_name_mtu(idx: u32) -> Result<(String, usize)> { + 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() } { + return Err(Error::last_os_error()); + } + // Convert to Rust string. + let name = unsafe { + CStr::from_ptr(name.as_ptr()) + .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()) +} + +#[repr(C)] +union SockaddrStorage { + sin: sockaddr_in, + sin6: sockaddr_in6, +} + +impl SockaddrStorage { + const fn len(&self) -> u8 { + unsafe { self.sin.sin_len } + } +} + +impl From for SockaddrStorage { + fn from(ip: IpAddr) -> Self { + match ip { + IpAddr::V4(ip) => SockaddrStorage { + sin: sockaddr_in { + #[allow(clippy::cast_possible_truncation)] + // `sockaddr_in` len is <= u8::MAX per `const_assert!` above. + sin_len: size_of::() as u8, + sin_family: AF_INET, + sin_addr: in_addr { + s_addr: u32::from_ne_bytes(ip.octets()), + }, + sin_port: 0, + sin_zero: [0; 8], + }, + }, + IpAddr::V6(ip) => SockaddrStorage { + sin6: sockaddr_in6 { + #[allow(clippy::cast_possible_truncation)] + // `sockaddr_in6` len is <= u8::MAX per `const_assert!` above. + sin6_len: size_of::() as u8, + sin6_family: AF_INET6, + sin6_addr: in6_addr { + s6_addr: ip.octets(), + }, + sin6_port: 0, + sin6_flowinfo: 0, + sin6_scope_id: 0, + }, + }, + } + } +} + +#[repr(C)] +struct RouteMessage { + rtm: rt_msghdr, + sa: SockaddrStorage, +} + +impl RouteMessage { + fn new(remote: IpAddr, seq: i32) -> Self { + let sa = SockaddrStorage::from(remote); + 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_version: RTM_VERSION, + rtm_type: RTM_GET, + rtm_seq: seq, + rtm_addrs: RTM_ADDRS, + ..Default::default() + }, + sa, + } + } + + const fn version(&self) -> u8 { + self.rtm.rtm_version + } + + const fn kind(&self) -> u8 { + self.rtm.rtm_type + } + + const fn len(&self) -> usize { + self.rtm.rtm_msglen as usize + } +} + +impl From<&RouteMessage> for &[u8] { + fn from(value: &RouteMessage) -> Self { + debug_assert!(value.len() >= size_of::()); + unsafe { slice::from_raw_parts(ptr::from_ref(value).cast(), value.len()) } + } +} + +impl From> for rt_msghdr { + fn from(value: Vec) -> Self { + debug_assert!(value.len() >= size_of::()); + unsafe { ptr::read_unaligned(value.as_ptr().cast()) } + } +} + +fn if_index(remote: IpAddr) -> Result { + // 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_version = query.version(); + let query_type = query.kind(); + fd.write_all((&query).into())?; + + // Read route messages. + let pid = unsafe { getpid() }; + loop { + let mut buf = vec![ + 0u8; + size_of::() + + // There will never be `RTAX_MAX` sockaddrs attached, but it's a safe upper bound. + (RTAX_MAX as usize * size_of::()) + ]; + let len = fd.read(&mut buf[..])?; + 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 + { + // 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()) + }; + } + } +} + +pub fn interface_and_mtu_impl(remote: IpAddr) -> Result<(String, usize)> { + let if_index = if_index(remote)?; + if_name_mtu(if_index.into()) +} diff --git a/src/lib.rs b/src/lib.rs index 5972552b..5f856955 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,18 +9,16 @@ //! //! # Usage //! -//! This crate exports a single function `interface_and_mtu` that, given a pair of local and remote `SocketAddr`s, returns the name and [maximum transmission unit (MTU)](https://en.wikipedia.org/wiki/Maximum_transmission_unit) of the local network interface used by a socket bound to the local address and connected towards the remote destination. -//! -//! If the local address is `None`, the function will let the operating system choose the local -//! address based on the given remote address. If the remote address is `None`, the function will -//! return the name and MTU of the local network interface with the given local address. +//! This crate exports a single function `interface_and_mtu` that returns the name and +//! [maximum transmission unit (MTU)](https://en.wikipedia.org/wiki/Maximum_transmission_unit) +//! of the outgoing network interface towards a remote destination identified by an `IpAddr`. //! //! # Example -//! -//! ```rust -//! let saddr = "127.0.0.1:443".parse().unwrap(); -//! let (name, mtu) = mtu::interface_and_mtu(&(None, saddr)).unwrap(); -//! println!("MTU for {saddr:?} is {mtu} on {name}"); +//! ``` +//! # use std::net::{IpAddr, Ipv4Addr}; +//! let destination = IpAddr::V4(Ipv4Addr::LOCALHOST); +//! let (name, mtu): (String, usize) = mtu::interface_and_mtu(destination).unwrap(); +//! println!("MTU towards {destination} is {mtu} on {name}"); //! ``` //! //! # Supported Platforms @@ -45,330 +43,82 @@ //! guidelines](CODE_OF_CONDUCT.md) beforehand. use std::{ - io::{Error, ErrorKind}, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}, + io::{Error, ErrorKind, Result}, + net::IpAddr, }; -// Though the module includes `allow(clippy::all)`, that doesn't seem to affect some lints -#[allow(clippy::semicolon_if_nothing_returned, clippy::struct_field_names)] -#[cfg(windows)] -mod win_bindings; - -/// Prepare a default error result. -fn default_result() -> Result<(String, T), Error> { - Err(Error::new( - ErrorKind::NotFound, - "Local interface MTU not found", - )) +#[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!`. + const $name: $t1 = $e as $t1; + const_assert_eq!($name as $t2, $e); + }; } -#[derive(Debug)] -pub enum SocketAddrs { - Local(IpAddr), - Remote(SocketAddr), - Both((IpAddr, SocketAddr)), -} +#[cfg(any(apple, bsd))] +mod bsd; -impl From<&(IpAddr, SocketAddr)> for SocketAddrs { - fn from((local, remote): &(IpAddr, SocketAddr)) -> Self { - Self::Both((*local, *remote)) - } +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(not(target_os = "windows"))] +mod routesocket; + +#[cfg(any(apple, bsd))] +use bsd::interface_and_mtu_impl; +#[cfg(target_os = "linux")] +use linux::interface_and_mtu_impl; +#[cfg(target_os = "windows")] +use windows::interface_and_mtu_impl; + +/// Prepare a default error. +fn default_err() -> Error { + Error::new(ErrorKind::NotFound, "Local interface MTU not found") } -impl From<&(Option, SocketAddr)> for SocketAddrs { - fn from((local, remote): &(Option, SocketAddr)) -> Self { - local.map_or(Self::Remote(*remote), |local| Self::Both((local, *remote))) - } +/// Prepare an error for cases that "should never happen". +#[cfg(not(target_os = "windows"))] +fn unlikely_err(msg: String) -> Error { + debug_assert!(false, "{msg}"); + Error::new(ErrorKind::Other, msg) } -impl From<&(IpAddr, Option)> for SocketAddrs { - fn from((local, remote): &(IpAddr, Option)) -> Self { - remote.map_or(Self::Local(*local), |remote| Self::Both((*local, remote))) +/// Align `size` to the next multiple of `align` (which needs to be a power of two). +#[cfg(not(target_os = "windows"))] +const fn aligned_by(size: usize, align: usize) -> usize { + if size == 0 { + align + } else { + 1 + ((size - 1) | (align - 1)) } } -/// Return the name and maximum transmission unit (MTU) of a local network interface. -/// -/// Given a pair of local and remote [`SocketAddr`]s, return the name and maximum -/// transmission unit (MTU) of the local network interface used by a socket bound to the local -/// address and connected towards the remote destination. +/// Return the name and maximum transmission unit (MTU) of the outgoing network interface towards a +/// remote destination identified by an [`IpAddr`], /// -/// If the local address is `None`, the function will let the operating system choose the local -/// address based on the given remote address. If the remote address is `None`, the function will -/// return the MTU of the local network interface with the given local address. -/// -/// The returned MTU may exceed the maximum IP packet size of 65,535 bytes on some -/// platforms for some remote destinations. (For example, loopback destinations on -/// Windows.) +/// The returned MTU may exceed the maximum IP packet size of 65,535 bytes on some platforms for +/// some remote destinations. (For example, loopback destinations on Windows.) /// /// The returned interface name is obtained from the operating system. /// -/// # Examples -/// -/// ``` -/// let saddr = "127.0.0.1:443".parse().unwrap(); -/// let (name, mtu) = mtu::interface_and_mtu(&(None, saddr)).unwrap(); -/// println!("MTU towards {saddr:?} is {mtu} on {name}"); -/// ``` -/// /// # Errors /// /// This function returns an error if the local interface MTU cannot be determined. -pub fn interface_and_mtu(addrs: A) -> Result<(String, usize), Error> -where - SocketAddrs: From, -{ - let addrs = SocketAddrs::from(addrs); - let local = match addrs { - SocketAddrs::Local(local) | SocketAddrs::Both((local, _)) => local, - SocketAddrs::Remote(remote) => { - if remote.is_ipv4() { - IpAddr::V4(Ipv4Addr::UNSPECIFIED) - } else { - IpAddr::V6(Ipv6Addr::UNSPECIFIED) - } - } - }; - // Let the OS choose an unused local port. - let socket = UdpSocket::bind(SocketAddr::new(local, 0))?; - match addrs { - SocketAddrs::Local(_) => {} - SocketAddrs::Remote(remote) | SocketAddrs::Both((_, remote)) => { - socket.connect(remote)?; - } - } - interface_and_mtu_impl(&socket) -} - -#[cfg(not(any( - target_os = "macos", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "linux", - target_os = "windows" -)))] -fn interface_and_mtu_impl(_socket: &UdpSocket) -> Result<(String, usize), Error> { - default_result() -} - -#[cfg(any( - target_os = "macos", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "linux" -))] -fn interface_and_mtu_impl(socket: &UdpSocket) -> Result<(String, usize), Error> { - use std::{ - ffi::{c_int, CStr}, - ptr, - }; - #[cfg(target_os = "linux")] - use std::{mem, os::fd::AsRawFd}; - - use libc::{ - freeifaddrs, getifaddrs, ifaddrs, in_addr_t, sockaddr_in, sockaddr_in6, AF_INET, AF_INET6, - }; - #[cfg(any( - target_os = "macos", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - use libc::{if_data, AF_LINK}; - #[cfg(target_os = "linux")] - use libc::{ifreq, ioctl}; - - // Get the interface list. - let mut ifap: *mut ifaddrs = ptr::null_mut(); - if unsafe { getifaddrs(&mut ifap) } != 0 { - return Err(Error::last_os_error()); - } - let ifap = ifap; // Do not modify this pointer. - - // First, find the name of the interface with the local IP address determined above. - let mut cursor = ifap; - let iface = loop { - if cursor.is_null() { - break None; - } - - let ifa = unsafe { &*cursor }; - if !ifa.ifa_addr.is_null() { - let saddr = unsafe { &*ifa.ifa_addr }; - if matches!(c_int::from(saddr.sa_family), AF_INET | AF_INET6) - && match socket.local_addr()?.ip() { - IpAddr::V4(ip) => { - let saddr: sockaddr_in = - unsafe { ptr::read_unaligned(ifa.ifa_addr.cast::()) }; - saddr.sin_addr.s_addr == in_addr_t::to_be(ip.into()) - } - IpAddr::V6(ip) => { - let saddr: sockaddr_in6 = - unsafe { ptr::read_unaligned(ifa.ifa_addr.cast::()) }; - saddr.sin6_addr.s6_addr == ip.octets() - } - } - { - break unsafe { CStr::from_ptr(ifa.ifa_name).to_str().ok() }; - } - } - cursor = ifa.ifa_next; - }; - - // If we have found the interface name we are looking for, find the MTU. - let mut res = default_result(); - if let Some(iface) = iface { - #[cfg(any( - target_os = "macos", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - // On macOS, we need to loop again to find the MTU of that interface. We need to - // do two loops, because `getifaddrs` returns one entry per - // interface and link type, and the IP addresses are in the - // AF_INET/AF_INET6 entries for an interface, whereas the - // MTU is (only) in the AF_LINK entry, whose `ifa_addr` - // contains MAC address information, not IP address - // information. - let mut cursor = ifap; - while !cursor.is_null() { - let ifa = unsafe { &*cursor }; - if !ifa.ifa_addr.is_null() { - let saddr = unsafe { &*ifa.ifa_addr }; - let name = - String::from_utf8_lossy(unsafe { CStr::from_ptr(ifa.ifa_name).to_bytes() }); - if c_int::from(saddr.sa_family) == AF_LINK - && !ifa.ifa_data.is_null() - && name == iface - { - let data = unsafe { &*(ifa.ifa_data as *const if_data) }; - if let Ok(mtu) = usize::try_from(data.ifi_mtu) { - res = Ok((iface.to_string(), mtu)); - } - break; - } - } - cursor = ifa.ifa_next; - } - } - - #[cfg(target_os = "linux")] - { - // On Linux, we can get the MTU via an ioctl on the socket. - let mut ifr: ifreq = unsafe { mem::zeroed() }; - ifr.ifr_name[..iface.len()] - .copy_from_slice(unsafe { &*ptr::from_ref::<[u8]>(iface.as_bytes()) }); - if unsafe { ioctl(socket.as_raw_fd(), libc::SIOCGIFMTU, &ifr) } != 0 { - res = Err(Error::last_os_error()); - } else if let Ok(mtu) = usize::try_from(unsafe { ifr.ifr_ifru.ifru_mtu }) { - res = Ok((iface.to_string(), mtu)); - } - } - } - - unsafe { freeifaddrs(ifap) }; - res -} - -#[cfg(target_os = "windows")] -fn interface_and_mtu_impl(socket: &UdpSocket) -> Result<(String, usize), Error> { - use std::{ - ffi::{c_void, CStr}, - ptr, slice, - }; - - use win_bindings::{ - if_indextoname, FreeMibTable, GetIpInterfaceTable, GetUnicastIpAddressTable, AF_INET, - AF_INET6, AF_UNSPEC, MIB_IPINTERFACE_ROW, MIB_IPINTERFACE_TABLE, MIB_UNICASTIPADDRESS_ROW, - MIB_UNICASTIPADDRESS_TABLE, NO_ERROR, - }; - - let mut res = default_result(); - - // Get a list of all unicast IP addresses with associated metadata. - let mut addr_table: *mut MIB_UNICASTIPADDRESS_TABLE = ptr::null_mut(); - if unsafe { GetUnicastIpAddressTable(AF_UNSPEC, &mut addr_table) } != NO_ERROR { - return Err(Error::last_os_error()); - } - let addr_table = addr_table; // Do not modify this pointer. - - let addrs = unsafe { - slice::from_raw_parts::( - &(*addr_table).Table[0], - (*addr_table).NumEntries as usize, - ) - }; - - // Get a list of all interfaces with associated metadata. - let mut if_table: *mut MIB_IPINTERFACE_TABLE = ptr::null_mut(); - if unsafe { GetIpInterfaceTable(AF_UNSPEC, &mut if_table) } != NO_ERROR { - let error = Error::last_os_error(); - unsafe { FreeMibTable(addr_table as *const c_void) }; - return Err(error); - } - let if_table = if_table; // Do not modify this pointer. - - let ifaces = unsafe { - slice::from_raw_parts::( - &(*if_table).Table[0], - (*if_table).NumEntries as usize, - ) - }; - - // Run through the list of addresses and find the one that matches the local IP - // address. - 'addr_loop: for addr in addrs { - let af = unsafe { addr.Address.si_family }; - let ip = socket.local_addr()?.ip(); - if (af == AF_INET && ip.is_ipv4() || af == AF_INET6 && ip.is_ipv6()) - && match ip { - IpAddr::V4(ip) => { - u32::from(ip).to_be() == unsafe { addr.Address.Ipv4.sin_addr.S_un.S_addr } - } - IpAddr::V6(ip) => ip.octets() == unsafe { addr.Address.Ipv6.sin6_addr.u.Byte }, - } - { - // For the matching address, find local interface and its MTU. - for iface in ifaces { - if iface.InterfaceIndex == addr.InterfaceIndex { - if let Ok(mtu) = iface.NlMtu.try_into() { - let mut name = [0u8; 256]; // IF_NAMESIZE not available? - if unsafe { !if_indextoname(iface.InterfaceIndex, &mut name).is_null() } { - if let Ok(name) = CStr::from_bytes_until_nul(&name) { - if let Ok(name) = name.to_str() { - res = Ok((name.to_string(), mtu)); - } - } - } else { - res = Err(Error::last_os_error()); - } - } - break 'addr_loop; - } - } - } - } - - unsafe { FreeMibTable(if_table as *const c_void) }; - unsafe { FreeMibTable(addr_table as *const c_void) }; - - res +pub fn interface_and_mtu(remote: IpAddr) -> Result<(String, usize)> { + interface_and_mtu_impl(remote) } #[cfg(test)] mod test { use std::{ env, - io::ErrorKind, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, }; - use rand::Rng; - use crate::interface_and_mtu; #[derive(Debug)] @@ -380,7 +130,7 @@ mod test { } } - #[cfg(any(target_os = "macos", target_os = "freebsd",))] + #[cfg(any(apple, target_os = "freebsd",))] const LOOPBACK: NameMtu = NameMtu(Some("lo0"), 16_384); #[cfg(target_os = "linux")] const LOOPBACK: NameMtu = NameMtu(Some("lo"), 65_536); @@ -394,141 +144,41 @@ mod test { // Non-loopback interface names are unpredictable, so we only check the MTU. const INET: NameMtu = NameMtu(None, 1_500); - // The tests can run in parallel, so try and find unused ports for all the tests. - fn socket_with_addr(local_ip: IpAddr) -> SocketAddr { - loop { - let port = rand::thread_rng().gen_range(1024..65535); - let saddr = SocketAddr::new(local_ip, port); - let socket = UdpSocket::bind(saddr); - match socket { - // We found an unused port. - Ok(socket) => return socket.local_addr().unwrap(), - Err(e) => match e.kind() { - ErrorKind::AddrInUse | ErrorKind::PermissionDenied => { - // We hit a used or priviledged port, try again. - continue; - } - _ => { - // We hit another error. Pretend that worked by returning the socket - // address, so the actual code can hit the same error. - return saddr; - } - }, - } - } - } - - fn local_v4() -> SocketAddr { - socket_with_addr(IpAddr::V4(Ipv4Addr::LOCALHOST)) - } - - fn local_v6() -> SocketAddr { - socket_with_addr(IpAddr::V6(Ipv6Addr::LOCALHOST)) - } - - fn inet_v4() -> SocketAddr { - // cloudflare.com - socket_with_addr(IpAddr::V4(Ipv4Addr::new(104, 16, 132, 229))) - } - - fn inet_v6() -> SocketAddr { - // cloudflare.com - socket_with_addr(IpAddr::V6(Ipv6Addr::new( - 0x26, 0x06, 0x47, 0x00, 0x68, 0x10, 0x84, 0xe5, - ))) - } - - #[test] - fn loopback_v4_loopback_v4() { - assert_eq!( - interface_and_mtu(&(local_v4().ip(), local_v4())).unwrap(), - LOOPBACK - ); - } - - #[test] - fn loopback_v4_loopback_v6() { - assert!(interface_and_mtu(&(local_v4().ip(), local_v6())).is_err()); - } - #[test] - fn loopback_v6_loopback_v4() { - assert!(interface_and_mtu(&(local_v6().ip(), local_v4())).is_err()); - } - - #[test] - fn loopback_v6_loopback_v6() { + fn loopback_v4() { assert_eq!( - interface_and_mtu(&(local_v6().ip(), local_v6())).unwrap(), + interface_and_mtu(IpAddr::V4(Ipv4Addr::LOCALHOST)).unwrap(), LOOPBACK ); } - #[test] - fn none_loopback_v4() { - assert_eq!(interface_and_mtu(&(None, local_v4())).unwrap(), LOOPBACK); - } - - #[test] - fn none_loopback_v6() { - assert_eq!(interface_and_mtu(&(None, local_v6())).unwrap(), LOOPBACK); - } #[test] - fn loopback_v4_none() { + fn loopback_v6() { assert_eq!( - interface_and_mtu(&(local_v4().ip(), None)).unwrap(), + interface_and_mtu(IpAddr::V6(Ipv6Addr::LOCALHOST)).unwrap(), LOOPBACK ); } #[test] - fn loopback_v6_none() { + fn inet_v4() { assert_eq!( - interface_and_mtu(&(local_v6().ip(), None)).unwrap(), - LOOPBACK + interface_and_mtu(IpAddr::V4(Ipv4Addr::new( + 104, 16, 132, 229 // cloudflare.com + ))) + .unwrap(), + INET ); } #[test] - fn inet_v4_inet_v4() { - assert!(interface_and_mtu(&(inet_v4().ip(), inet_v4())).is_err()); - } - - #[test] - fn inet_v4_inet_v6() { - assert!(interface_and_mtu(&(inet_v4().ip(), inet_v6())).is_err()); - } - - #[test] - fn inet_v6_inet_v4() { - assert!(interface_and_mtu(&(inet_v6().ip(), inet_v4())).is_err()); - } - - #[test] - fn inet_v6_inet_v6() { - assert!(interface_and_mtu(&(inet_v6().ip(), inet_v6())).is_err()); - } - #[test] - fn none_inet_v4() { - assert_eq!(interface_and_mtu(&(None, inet_v4())).unwrap(), INET); - } - - #[test] - fn none_inet_v6() { - if env::var("GITHUB_ACTIONS").is_ok() { + fn inet_v6() { + match interface_and_mtu(IpAddr::V6(Ipv6Addr::new( + 0x2606, 0x4700, 0, 0, 0, 0, 0x6810, 0x84e5, // cloudflare.com + ))) { + Ok(res) => assert_eq!(res, INET), // The GitHub CI environment does not have IPv6 connectivity. - return; + Err(_) => assert!(env::var("GITHUB_ACTIONS").is_ok()), } - assert_eq!(interface_and_mtu(&(None, inet_v6())).unwrap(), INET); - } - - #[test] - fn inet_v4_none() { - assert!(interface_and_mtu(&(inet_v4().ip(), None)).is_err()); - } - - #[test] - fn inet_v6_none() { - assert!(interface_and_mtu(&(inet_v6().ip(), None)).is_err()); } } diff --git a/src/linux.rs b/src/linux.rs new file mode 100644 index 00000000..bb5e4b09 --- /dev/null +++ b/src/linux.rs @@ -0,0 +1,348 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::{ + ffi::CStr, + io::{Error, ErrorKind, Read, Result, Write}, + mem::size_of, + net::IpAddr, + num::TryFromIntError, + ptr, slice, +}; + +use libc::{ + c_int, AF_NETLINK, ARPHRD_NONE, IFLA_IFNAME, IFLA_MTU, NETLINK_ROUTE, RTA_DST, RTA_OIF, + RTM_GETLINK, RTM_GETROUTE, RTM_NEWLINK, RTM_NEWROUTE, RTN_UNICAST, RT_SCOPE_UNIVERSE, + RT_TABLE_MAIN, +}; +use static_assertions::{const_assert, const_assert_eq}; + +use crate::{aligned_by, default_err, routesocket::RouteSocket, unlikely_err}; + +#[allow( + clippy::struct_field_names, + non_camel_case_types, + clippy::too_many_lines +)] +mod bindings { + include!(env!("BINDINGS")); +} + +use bindings::{ifinfomsg, nlmsghdr, rtattr, rtmsg}; + +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_UNSPEC, u8, libc::AF_UNSPEC, i32); +asserted_const_with_type!(NLM_F_REQUEST, u16, libc::NLM_F_REQUEST, c_int); +asserted_const_with_type!(NLM_F_ACK, u16, libc::NLM_F_ACK, c_int); +asserted_const_with_type!(NLMSG_ERROR, u16, libc::NLMSG_ERROR, c_int); + +const_assert!(size_of::() <= u8::MAX as usize); +const_assert!(size_of::() <= u8::MAX as usize); +const_assert!(size_of::() <= u8::MAX as usize); +const_assert!(size_of::() <= u8::MAX as usize); + +const NETLINK_BUFFER_SIZE: usize = 8192; // See netlink(7) man page. + +#[repr(C)] +enum AddrBytes { + V4([u8; 4]), + V6([u8; 16]), +} + +impl AddrBytes { + const fn new(ip: IpAddr) -> Self { + match ip { + IpAddr::V4(ip) => Self::V4(ip.octets()), + IpAddr::V6(ip) => Self::V6(ip.octets()), + } + } + + const fn len(&self) -> usize { + match self { + Self::V4(_) => 4, + Self::V6(_) => 16, + } + } +} + +impl From for [u8; 16] { + fn from(addr: AddrBytes) -> Self { + match addr { + AddrBytes::V4(bytes) => { + let mut v6 = [0; 16]; + v6[..4].copy_from_slice(&bytes); + v6 + } + AddrBytes::V6(bytes) => bytes, + } + } +} + +#[repr(C)] +#[derive(Default)] +struct IfIndexMsg { + nlmsg: nlmsghdr, + rtm: rtmsg, + rt: rtattr, + addr: [u8; 16], +} + +impl IfIndexMsg { + fn new(remote: IpAddr, nlmsg_seq: u32) -> Self { + let addr = AddrBytes::new(remote); + #[allow(clippy::cast_possible_truncation)] + // Structs lens are <= u8::MAX per `const_assert!`s above; `addr_bytes` is max. 16 for IPv6. + let nlmsg_len = + (size_of::() + size_of::() + size_of::() + addr.len()) as u32; + Self { + nlmsg: nlmsghdr { + nlmsg_len, + nlmsg_type: RTM_GETROUTE, + nlmsg_flags: NLM_F_REQUEST | NLM_F_ACK, + nlmsg_seq, + ..Default::default() + }, + rtm: rtmsg { + rtm_family: match remote { + IpAddr::V4(_) => AF_INET, + IpAddr::V6(_) => AF_INET6, + }, + rtm_dst_len: match remote { + IpAddr::V4(_) => 32, + IpAddr::V6(_) => 128, + }, + rtm_table: RT_TABLE_MAIN, + rtm_scope: RT_SCOPE_UNIVERSE, + rtm_type: RTN_UNICAST, + ..Default::default() + }, + rt: rtattr { + #[allow(clippy::cast_possible_truncation)] + // Structs len is <= u8::MAX per `const_assert!` above; `addr_bytes` is max. 16 for IPv6. + rta_len: (size_of::() + addr.len()) as u16, + rta_type: RTA_DST, + }, + addr: addr.into(), + } + } + + const fn len(&self) -> usize { + let len = self.nlmsg.nlmsg_len as usize; + debug_assert!(len <= size_of::()); + len + } +} + +impl From<&IfIndexMsg> for &[u8] { + fn from(value: &IfIndexMsg) -> Self { + unsafe { slice::from_raw_parts(ptr::from_ref(value).cast(), value.len()) } + } +} + +impl TryFrom<&[u8]> for nlmsghdr { + type Error = Error; + + fn try_from(value: &[u8]) -> Result { + if value.len() < size_of::() { + return Err(default_err()); + } + Ok(unsafe { ptr::read_unaligned(value.as_ptr().cast()) }) + } +} + +fn parse_c_int(buf: &[u8]) -> Result { + let bytes = <&[u8] as TryInto<[u8; size_of::()]>>::try_into(&buf[..size_of::()]) + .map_err(|_| default_err())?; + Ok(c_int::from_ne_bytes(bytes)) +} + +fn read_msg_with_seq(fd: &mut RouteSocket, seq: u32, kind: u16) -> Result<(nlmsghdr, Vec)> { + loop { + let buf = &mut [0u8; NETLINK_BUFFER_SIZE]; + let len = fd.read(buf.as_mut_slice())?; + let mut next = &buf[..len]; + while size_of::() <= next.len() { + let (hdr, mut msg) = next.split_at(size_of::()); + let hdr: nlmsghdr = hdr.try_into()?; + // `msg` has the remainder of this message plus any following messages. + // Strip those it off and assign them to `next`. + debug_assert!(size_of::() <= hdr.nlmsg_len as usize); + (msg, next) = msg.split_at(hdr.nlmsg_len as usize - size_of::()); + + if hdr.nlmsg_seq != seq { + continue; + } + + if hdr.nlmsg_type == NLMSG_ERROR { + // Extract the error code and return it. + let err = parse_c_int(msg)?; + if err != 0 { + return Err(Error::from_raw_os_error(-err)); + } + } else if hdr.nlmsg_type == kind { + // Return the header and the message. + return Ok((hdr, msg.to_vec())); + } + } + } +} + +impl TryFrom<&[u8]> for rtattr { + type Error = Error; + + fn try_from(value: &[u8]) -> Result { + if value.len() < size_of::() { + return Err(default_err()); + } + Ok(unsafe { ptr::read_unaligned(value.as_ptr().cast()) }) + } +} + +struct RtAttr<'a> { + hdr: rtattr, + msg: &'a [u8], +} + +impl<'a> RtAttr<'a> { + fn new(bytes: &'a [u8]) -> Result { + debug_assert!(bytes.len() >= size_of::()); + let (hdr, mut msg) = bytes.split_at(size_of::()); + let hdr: rtattr = hdr.try_into()?; + let aligned_len = aligned_by(hdr.rta_len.into(), 4); + debug_assert!(size_of::() <= aligned_len); + (msg, _) = msg.split_at(aligned_len - size_of::()); + Ok(Self { hdr, msg }) + } +} + +struct RtAttrs<'a>(&'a [u8]); + +impl<'a> Iterator for RtAttrs<'a> { + type Item = RtAttr<'a>; + + fn next(&mut self) -> Option { + if size_of::() <= self.0.len() { + let attr = RtAttr::new(self.0).ok()?; + let aligned_len = aligned_by(attr.hdr.rta_len.into(), 4); + debug_assert!(self.0.len() >= aligned_len); + self.0 = self.0.split_at(aligned_len).1; + Some(attr) + } else { + None + } + } +} + +fn if_index(remote: IpAddr, fd: &mut RouteSocket) -> Result { + // Send RTM_GETROUTE message to get the interface index associated with the destination. + let msg_seq = RouteSocket::new_seq(); + let msg = IfIndexMsg::new(remote, msg_seq); + fd.write_all((&msg).into())?; + + // Receive RTM_GETROUTE response. + let (_hdr, mut buf) = read_msg_with_seq(fd, msg_seq, RTM_NEWROUTE)?; + debug_assert!(size_of::() <= buf.len()); + let buf = buf.split_off(size_of::()); + + // Parse through the attributes to find the interface index. + for attr in RtAttrs(buf.as_slice()).by_ref() { + if attr.hdr.rta_type == RTA_OIF { + // We have our interface index. + return parse_c_int(attr.msg); + } + } + Err(default_err()) +} + +#[repr(C)] +struct IfInfoMsg { + nlmsg: nlmsghdr, + ifim: ifinfomsg, +} + +impl IfInfoMsg { + fn new(if_index: i32, nlmsg_seq: u32) -> Self { + #[allow(clippy::cast_possible_truncation)] + // Structs lens are <= u8::MAX per `const_assert!`s above. + let nlmsg_len = (size_of::() + size_of::()) as u32; + Self { + nlmsg: nlmsghdr { + nlmsg_len, + nlmsg_type: RTM_GETLINK, + nlmsg_flags: NLM_F_REQUEST | NLM_F_ACK, + nlmsg_seq, + ..Default::default() + }, + ifim: ifinfomsg { + ifi_family: AF_UNSPEC, + ifi_type: ARPHRD_NONE, + ifi_index: if_index, + ..Default::default() + }, + } + } + + const fn len(&self) -> usize { + self.nlmsg.nlmsg_len as usize + } +} + +impl From<&IfInfoMsg> for &[u8] { + fn from(value: &IfInfoMsg) -> Self { + debug_assert!(value.len() >= size_of::()); + unsafe { slice::from_raw_parts(ptr::from_ref(value).cast(), value.len()) } + } +} + +fn if_name_mtu(if_index: i32, fd: &mut RouteSocket) -> Result<(String, usize)> { + // Send RTM_GETLINK message to get interface information for the given interface index. + let msg_seq = RouteSocket::new_seq(); + let msg = IfInfoMsg::new(if_index, msg_seq); + fd.write_all((&msg).into())?; + + // Receive RTM_GETLINK response. + let (_hdr, mut buf) = read_msg_with_seq(fd, msg_seq, RTM_NEWLINK)?; + debug_assert!(size_of::() <= buf.len()); + let buf = buf.split_off(size_of::()); + + // Parse through the attributes to find the interface name and MTU. + let mut ifname = None; + let mut mtu = None; + for attr in RtAttrs(buf.as_slice()).by_ref() { + match attr.hdr.rta_type { + IFLA_IFNAME => { + let name = CStr::from_bytes_until_nul(attr.msg) + .map_err(|err| Error::new(ErrorKind::Other, err))?; + ifname = Some( + name.to_str() + .map_err(|err| Error::new(ErrorKind::Other, err))? + .to_string(), + ); + } + IFLA_MTU => { + mtu = Some( + parse_c_int(attr.msg)? + .try_into() + .map_err(|e: TryFromIntError| unlikely_err(e.to_string()))?, + ); + } + _ => (), + } + if let (Some(ifname), Some(mtu)) = (ifname.as_ref(), mtu.as_ref()) { + return Ok((ifname.clone(), *mtu)); + } + } + + Err(default_err()) +} + +pub fn interface_and_mtu_impl(remote: IpAddr) -> Result<(String, usize)> { + // Create a netlink socket. + let mut fd = RouteSocket::new(AF_NETLINK, NETLINK_ROUTE)?; + let if_index = if_index(remote, &mut fd)?; + if_name_mtu(if_index, &mut fd) +} diff --git a/src/routesocket.rs b/src/routesocket.rs new file mode 100644 index 00000000..ab992419 --- /dev/null +++ b/src/routesocket.rs @@ -0,0 +1,81 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::{ + io::{Error, Read, Result, Write}, + num::TryFromIntError, + os::fd::{AsRawFd, FromRawFd, OwnedFd}, + sync::atomic::Ordering, +}; + +use libc::{fsync, read, socket, write, SOCK_RAW}; + +use crate::unlikely_err; + +#[cfg(target_os = "linux")] +type AtomicRouteSocketSeq = std::sync::atomic::AtomicU32; +#[cfg(target_os = "linux")] +type RouteSocketSeq = u32; + +#[cfg(not(target_os = "linux"))] +type AtomicRouteSocketSeq = std::sync::atomic::AtomicI32; +#[cfg(not(target_os = "linux"))] +type RouteSocketSeq = i32; + +static SEQ: AtomicRouteSocketSeq = AtomicRouteSocketSeq::new(0); + +pub struct RouteSocket(OwnedFd); + +impl RouteSocket { + pub fn new(domain: libc::c_int, protocol: libc::c_int) -> Result { + let fd = unsafe { socket(domain, SOCK_RAW, protocol) }; + if fd == -1 { + return Err(Error::last_os_error()); + } + Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) + } + + pub fn new_seq() -> RouteSocketSeq { + SEQ.fetch_add(1, Ordering::Relaxed) + } +} + +impl AsRawFd for RouteSocket { + fn as_raw_fd(&self) -> i32 { + self.0.as_raw_fd() + } +} + +fn check_result(res: isize) -> Result { + if res == -1 { + Err(Error::last_os_error()) + } else { + Ok(res + .try_into() + .map_err(|e: TryFromIntError| unlikely_err(e.to_string()))?) + } +} + +impl Write for RouteSocket { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let res = unsafe { write(self.as_raw_fd(), buf.as_ptr().cast(), buf.len()) }; + check_result(res) + } + + fn flush(&mut self) -> std::io::Result<()> { + let res = unsafe { fsync(self.as_raw_fd()) }; + check_result(res as isize).and(Ok(())) + } +} + +impl Read for RouteSocket { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + // If we've written a well-formed message into the kernel via `write`, we should be able to + // read a well-formed message back out, and not block. + let res = unsafe { read(self.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) }; + check_result(res) + } +} diff --git a/src/win_bindings.rs b/src/win_bindings.rs deleted file mode 100644 index 107b5c67..00000000 --- a/src/win_bindings.rs +++ /dev/null @@ -1,460 +0,0 @@ -// Bindings generated by `windows-bindgen` 0.58.0 - -#![allow( - non_snake_case, - non_upper_case_globals, - non_camel_case_types, - dead_code, - clippy::all -)] -#[inline] -pub unsafe fn FreeMibTable(memory: *const core::ffi::c_void) { - windows_targets::link!("iphlpapi.dll" "system" fn FreeMibTable(memory : *const core::ffi::c_void)); - FreeMibTable(memory) -} -#[inline] -pub unsafe fn GetIpInterfaceTable( - family: ADDRESS_FAMILY, - table: *mut *mut MIB_IPINTERFACE_TABLE, -) -> WIN32_ERROR { - windows_targets::link!("iphlpapi.dll" "system" fn GetIpInterfaceTable(family : ADDRESS_FAMILY, table : *mut *mut MIB_IPINTERFACE_TABLE) -> WIN32_ERROR); - GetIpInterfaceTable(family, table) -} -#[inline] -pub unsafe fn GetUnicastIpAddressTable( - family: ADDRESS_FAMILY, - table: *mut *mut MIB_UNICASTIPADDRESS_TABLE, -) -> WIN32_ERROR { - windows_targets::link!("iphlpapi.dll" "system" fn GetUnicastIpAddressTable(family : ADDRESS_FAMILY, table : *mut *mut MIB_UNICASTIPADDRESS_TABLE) -> WIN32_ERROR); - GetUnicastIpAddressTable(family, table) -} -#[inline] -pub unsafe fn if_indextoname( - interfaceindex: u32, - interfacename: &mut [u8; 256], -) -> windows_core::PSTR { - windows_targets::link!("iphlpapi.dll" "system" fn if_indextoname(interfaceindex : u32, interfacename : windows_core::PSTR) -> windows_core::PSTR); - if_indextoname(interfaceindex, core::mem::transmute(interfacename.as_ptr())) -} -#[repr(transparent)] -#[derive(PartialEq, Eq, Copy, Clone, Default)] -pub struct ADDRESS_FAMILY(pub u16); -impl windows_core::TypeKind for ADDRESS_FAMILY { - type TypeKind = windows_core::CopyType; -} -impl core::fmt::Debug for ADDRESS_FAMILY { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("ADDRESS_FAMILY").field(&self.0).finish() - } -} -pub const AF_INET: ADDRESS_FAMILY = ADDRESS_FAMILY(2u16); -pub const AF_INET6: ADDRESS_FAMILY = ADDRESS_FAMILY(23u16); -pub const AF_UNSPEC: ADDRESS_FAMILY = ADDRESS_FAMILY(0u16); -#[repr(transparent)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct BOOLEAN(pub u8); -impl Default for BOOLEAN { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -impl windows_core::TypeKind for BOOLEAN { - type TypeKind = windows_core::CopyType; -} -#[repr(C)] -#[derive(Clone, Copy)] -pub struct IN6_ADDR { - pub u: IN6_ADDR_0, -} -impl windows_core::TypeKind for IN6_ADDR { - type TypeKind = windows_core::CopyType; -} -impl Default for IN6_ADDR { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub union IN6_ADDR_0 { - pub Byte: [u8; 16], - pub Word: [u16; 8], -} -impl windows_core::TypeKind for IN6_ADDR_0 { - type TypeKind = windows_core::CopyType; -} -impl Default for IN6_ADDR_0 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub struct IN_ADDR { - pub S_un: IN_ADDR_0, -} -impl windows_core::TypeKind for IN_ADDR { - type TypeKind = windows_core::CopyType; -} -impl Default for IN_ADDR { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub union IN_ADDR_0 { - pub S_un_b: IN_ADDR_0_0, - pub S_un_w: IN_ADDR_0_1, - pub S_addr: u32, -} -impl windows_core::TypeKind for IN_ADDR_0 { - type TypeKind = windows_core::CopyType; -} -impl Default for IN_ADDR_0 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct IN_ADDR_0_0 { - pub s_b1: u8, - pub s_b2: u8, - pub s_b3: u8, - pub s_b4: u8, -} -impl windows_core::TypeKind for IN_ADDR_0_0 { - type TypeKind = windows_core::CopyType; -} -impl Default for IN_ADDR_0_0 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct IN_ADDR_0_1 { - pub s_w1: u16, - pub s_w2: u16, -} -impl windows_core::TypeKind for IN_ADDR_0_1 { - type TypeKind = windows_core::CopyType; -} -impl Default for IN_ADDR_0_1 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub struct MIB_IPINTERFACE_ROW { - pub Family: ADDRESS_FAMILY, - pub InterfaceLuid: NET_LUID_LH, - pub InterfaceIndex: u32, - pub MaxReassemblySize: u32, - pub InterfaceIdentifier: u64, - pub MinRouterAdvertisementInterval: u32, - pub MaxRouterAdvertisementInterval: u32, - pub AdvertisingEnabled: BOOLEAN, - pub ForwardingEnabled: BOOLEAN, - pub WeakHostSend: BOOLEAN, - pub WeakHostReceive: BOOLEAN, - pub UseAutomaticMetric: BOOLEAN, - pub UseNeighborUnreachabilityDetection: BOOLEAN, - pub ManagedAddressConfigurationSupported: BOOLEAN, - pub OtherStatefulConfigurationSupported: BOOLEAN, - pub AdvertiseDefaultRoute: BOOLEAN, - pub RouterDiscoveryBehavior: NL_ROUTER_DISCOVERY_BEHAVIOR, - pub DadTransmits: u32, - pub BaseReachableTime: u32, - pub RetransmitTime: u32, - pub PathMtuDiscoveryTimeout: u32, - pub LinkLocalAddressBehavior: NL_LINK_LOCAL_ADDRESS_BEHAVIOR, - pub LinkLocalAddressTimeout: u32, - pub ZoneIndices: [u32; 16], - pub SitePrefixLength: u32, - pub Metric: u32, - pub NlMtu: u32, - pub Connected: BOOLEAN, - pub SupportsWakeUpPatterns: BOOLEAN, - pub SupportsNeighborDiscovery: BOOLEAN, - pub SupportsRouterDiscovery: BOOLEAN, - pub ReachableTime: u32, - pub TransmitOffload: NL_INTERFACE_OFFLOAD_ROD, - pub ReceiveOffload: NL_INTERFACE_OFFLOAD_ROD, - pub DisableDefaultRoutes: BOOLEAN, -} -impl windows_core::TypeKind for MIB_IPINTERFACE_ROW { - type TypeKind = windows_core::CopyType; -} -impl Default for MIB_IPINTERFACE_ROW { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub struct MIB_IPINTERFACE_TABLE { - pub NumEntries: u32, - pub Table: [MIB_IPINTERFACE_ROW; 1], -} -impl windows_core::TypeKind for MIB_IPINTERFACE_TABLE { - type TypeKind = windows_core::CopyType; -} -impl Default for MIB_IPINTERFACE_TABLE { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub struct MIB_UNICASTIPADDRESS_ROW { - pub Address: SOCKADDR_INET, - pub InterfaceLuid: NET_LUID_LH, - pub InterfaceIndex: u32, - pub PrefixOrigin: NL_PREFIX_ORIGIN, - pub SuffixOrigin: NL_SUFFIX_ORIGIN, - pub ValidLifetime: u32, - pub PreferredLifetime: u32, - pub OnLinkPrefixLength: u8, - pub SkipAsSource: BOOLEAN, - pub DadState: NL_DAD_STATE, - pub ScopeId: SCOPE_ID, - pub CreationTimeStamp: i64, -} -impl windows_core::TypeKind for MIB_UNICASTIPADDRESS_ROW { - type TypeKind = windows_core::CopyType; -} -impl Default for MIB_UNICASTIPADDRESS_ROW { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub struct MIB_UNICASTIPADDRESS_TABLE { - pub NumEntries: u32, - pub Table: [MIB_UNICASTIPADDRESS_ROW; 1], -} -impl windows_core::TypeKind for MIB_UNICASTIPADDRESS_TABLE { - type TypeKind = windows_core::CopyType; -} -impl Default for MIB_UNICASTIPADDRESS_TABLE { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub union NET_LUID_LH { - pub Value: u64, - pub Info: NET_LUID_LH_0, -} -impl windows_core::TypeKind for NET_LUID_LH { - type TypeKind = windows_core::CopyType; -} -impl Default for NET_LUID_LH { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct NET_LUID_LH_0 { - pub _bitfield: u64, -} -impl windows_core::TypeKind for NET_LUID_LH_0 { - type TypeKind = windows_core::CopyType; -} -impl Default for NET_LUID_LH_0 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(transparent)] -#[derive(PartialEq, Eq, Copy, Clone, Default)] -pub struct NL_DAD_STATE(pub i32); -impl windows_core::TypeKind for NL_DAD_STATE { - type TypeKind = windows_core::CopyType; -} -impl core::fmt::Debug for NL_DAD_STATE { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("NL_DAD_STATE").field(&self.0).finish() - } -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct NL_INTERFACE_OFFLOAD_ROD { - pub _bitfield: u8, -} -impl windows_core::TypeKind for NL_INTERFACE_OFFLOAD_ROD { - type TypeKind = windows_core::CopyType; -} -impl Default for NL_INTERFACE_OFFLOAD_ROD { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(transparent)] -#[derive(PartialEq, Eq, Copy, Clone, Default)] -pub struct NL_LINK_LOCAL_ADDRESS_BEHAVIOR(pub i32); -impl windows_core::TypeKind for NL_LINK_LOCAL_ADDRESS_BEHAVIOR { - type TypeKind = windows_core::CopyType; -} -impl core::fmt::Debug for NL_LINK_LOCAL_ADDRESS_BEHAVIOR { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("NL_LINK_LOCAL_ADDRESS_BEHAVIOR") - .field(&self.0) - .finish() - } -} -#[repr(transparent)] -#[derive(PartialEq, Eq, Copy, Clone, Default)] -pub struct NL_PREFIX_ORIGIN(pub i32); -impl windows_core::TypeKind for NL_PREFIX_ORIGIN { - type TypeKind = windows_core::CopyType; -} -impl core::fmt::Debug for NL_PREFIX_ORIGIN { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("NL_PREFIX_ORIGIN").field(&self.0).finish() - } -} -#[repr(transparent)] -#[derive(PartialEq, Eq, Copy, Clone, Default)] -pub struct NL_ROUTER_DISCOVERY_BEHAVIOR(pub i32); -impl windows_core::TypeKind for NL_ROUTER_DISCOVERY_BEHAVIOR { - type TypeKind = windows_core::CopyType; -} -impl core::fmt::Debug for NL_ROUTER_DISCOVERY_BEHAVIOR { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("NL_ROUTER_DISCOVERY_BEHAVIOR") - .field(&self.0) - .finish() - } -} -#[repr(transparent)] -#[derive(PartialEq, Eq, Copy, Clone, Default)] -pub struct NL_SUFFIX_ORIGIN(pub i32); -impl windows_core::TypeKind for NL_SUFFIX_ORIGIN { - type TypeKind = windows_core::CopyType; -} -impl core::fmt::Debug for NL_SUFFIX_ORIGIN { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("NL_SUFFIX_ORIGIN").field(&self.0).finish() - } -} -pub const NO_ERROR: WIN32_ERROR = WIN32_ERROR(0u32); -#[repr(C)] -#[derive(Clone, Copy)] -pub struct SCOPE_ID { - pub Anonymous: SCOPE_ID_0, -} -impl windows_core::TypeKind for SCOPE_ID { - type TypeKind = windows_core::CopyType; -} -impl Default for SCOPE_ID { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub union SCOPE_ID_0 { - pub Anonymous: SCOPE_ID_0_0, - pub Value: u32, -} -impl windows_core::TypeKind for SCOPE_ID_0 { - type TypeKind = windows_core::CopyType; -} -impl Default for SCOPE_ID_0 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct SCOPE_ID_0_0 { - pub _bitfield: u32, -} -impl windows_core::TypeKind for SCOPE_ID_0_0 { - type TypeKind = windows_core::CopyType; -} -impl Default for SCOPE_ID_0_0 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub struct SOCKADDR_IN { - pub sin_family: ADDRESS_FAMILY, - pub sin_port: u16, - pub sin_addr: IN_ADDR, - pub sin_zero: [i8; 8], -} -impl windows_core::TypeKind for SOCKADDR_IN { - type TypeKind = windows_core::CopyType; -} -impl Default for SOCKADDR_IN { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub struct SOCKADDR_IN6 { - pub sin6_family: ADDRESS_FAMILY, - pub sin6_port: u16, - pub sin6_flowinfo: u32, - pub sin6_addr: IN6_ADDR, - pub Anonymous: SOCKADDR_IN6_0, -} -impl windows_core::TypeKind for SOCKADDR_IN6 { - type TypeKind = windows_core::CopyType; -} -impl Default for SOCKADDR_IN6 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub union SOCKADDR_IN6_0 { - pub sin6_scope_id: u32, - pub sin6_scope_struct: SCOPE_ID, -} -impl windows_core::TypeKind for SOCKADDR_IN6_0 { - type TypeKind = windows_core::CopyType; -} -impl Default for SOCKADDR_IN6_0 { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(C)] -#[derive(Clone, Copy)] -pub union SOCKADDR_INET { - pub Ipv4: SOCKADDR_IN, - pub Ipv6: SOCKADDR_IN6, - pub si_family: ADDRESS_FAMILY, -} -impl windows_core::TypeKind for SOCKADDR_INET { - type TypeKind = windows_core::CopyType; -} -impl Default for SOCKADDR_INET { - fn default() -> Self { - unsafe { core::mem::zeroed() } - } -} -#[repr(transparent)] -#[derive(PartialEq, Eq, Copy, Clone, Default)] -pub struct WIN32_ERROR(pub u32); -impl windows_core::TypeKind for WIN32_ERROR { - type TypeKind = windows_core::CopyType; -} -impl core::fmt::Debug for WIN32_ERROR { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("WIN32_ERROR").field(&self.0).finish() - } -} diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 00000000..1c76f26a --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,143 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::{ + ffi::CStr, + io::{Error, ErrorKind, Result}, + net::IpAddr, + ptr, slice, +}; + +use crate::default_err; + +#[allow( + non_camel_case_types, + non_snake_case, + clippy::semicolon_if_nothing_returned, + clippy::missing_transmute_annotations, + clippy::upper_case_acronyms, + clippy::struct_field_names +)] +mod bindings { + include!(env!("BINDINGS")); +} + +use bindings::{ + if_indextoname, FreeMibTable, GetBestInterfaceEx, GetIpInterfaceTable, AF_INET, AF_INET6, + AF_UNSPEC, IF_MAX_STRING_SIZE, IN6_ADDR, IN6_ADDR_0, IN_ADDR, IN_ADDR_0, MIB_IPINTERFACE_ROW, + MIB_IPINTERFACE_TABLE, NO_ERROR, SOCKADDR, SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_INET, +}; + +struct MibTablePtr(*mut MIB_IPINTERFACE_TABLE); + +impl MibTablePtr { + fn mut_ptr_ptr(&mut self) -> *mut *mut MIB_IPINTERFACE_TABLE { + ptr::from_mut(&mut self.0) + } +} + +impl Default for MibTablePtr { + fn default() -> Self { + Self(ptr::null_mut()) + } +} + +impl Drop for MibTablePtr { + fn drop(&mut self) { + if !self.0.is_null() { + // Free the memory allocated by GetIpInterfaceTable. + unsafe { FreeMibTable(self.0.cast()) }; + } + } +} + +pub fn interface_and_mtu_impl(remote: IpAddr) -> Result<(String, usize)> { + // Convert remote to Windows SOCKADDR_INET format. The SOCKADDR_INET union contains an IPv4 or + // an IPv6 address. + // + // See https://learn.microsoft.com/en-us/windows/win32/api/ws2ipdef/ns-ws2ipdef-sockaddr_inet + let dst = match remote { + IpAddr::V4(ip) => { + // Initialize the `SOCKADDR_IN` variant of `SOCKADDR_INET` based on `ip`. + SOCKADDR_INET { + Ipv4: SOCKADDR_IN { + sin_family: AF_INET, + sin_addr: IN_ADDR { + S_un: IN_ADDR_0 { + S_addr: u32::to_be(ip.into()), + }, + }, + ..Default::default() + }, + } + } + IpAddr::V6(ip) => { + // Initialize the `SOCKADDR_IN6` variant of `SOCKADDR_INET` based on `ip`. + SOCKADDR_INET { + Ipv6: SOCKADDR_IN6 { + sin6_family: AF_INET6, + sin6_addr: IN6_ADDR { + u: IN6_ADDR_0 { Byte: ip.octets() }, + }, + ..Default::default() + }, + } + } + }; + + // Get the interface index of the best outbound interface towards `dst`. + let mut idx = 0; + let res = unsafe { + // We're now casting `&dst` to a `SOCKADDR` pointer. This is OK based on + // https://learn.microsoft.com/en-us/windows/win32/winsock/sockaddr-2. + // With that, we call `GetBestInterfaceEx` to get the interface index into `idx`. + // See https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getbestinterfaceex + GetBestInterfaceEx( + ptr::from_ref(&dst).cast::(), + ptr::from_mut(&mut idx), + ) + }; + if res != 0 { + return Err(Error::last_os_error()); + } + + // Get a list of all interfaces with associated metadata. + let mut if_table = MibTablePtr::default(); + // GetIpInterfaceTable allocates memory, which MibTablePtr::drop will free. + if unsafe { GetIpInterfaceTable(AF_UNSPEC, if_table.mut_ptr_ptr()) } != NO_ERROR { + return Err(Error::last_os_error()); + } + // Make a slice + let ifaces = unsafe { + slice::from_raw_parts::( + &(*if_table.0).Table[0], + (*if_table.0).NumEntries as usize, + ) + }; + + // Find the local interface matching `idx`. + for iface in ifaces { + if iface.InterfaceIndex == idx { + // Get the MTU. + let mtu: usize = iface.NlMtu.try_into().map_err(|_| default_err())?; + // Get the interface name. + let mut interfacename = [0u8; IF_MAX_STRING_SIZE as usize]; + // if_indextoname writes into the provided buffer. + if unsafe { if_indextoname(iface.InterfaceIndex, &mut interfacename).is_null() } { + return Err(default_err()); + } + // Convert the interface name to a Rust string. + let name = CStr::from_bytes_until_nul(interfacename.as_ref()) + .map_err(|_| default_err())? + .to_str() + .map_err(|err| Error::new(ErrorKind::Other, err))? + .to_string(); + // We found our interface information. + return Ok((name, mtu)); + } + } + Err(default_err()) +} diff --git a/tests/win_bindings.rs b/tests/win_bindings.rs deleted file mode 100644 index 1367397e..00000000 --- a/tests/win_bindings.rs +++ /dev/null @@ -1,39 +0,0 @@ -#![cfg(windows)] - -use std::fs; - -#[test] -fn codegen_windows_bindings() { - let existing = fs::read_to_string(TARGET).unwrap_or_default(); - windows_bindgen::bindgen([ - "--out", - TARGET, - "--config", - "flatten", - "--filter", - "Windows.Win32.Foundation.NO_ERROR", - "Windows.Win32.NetworkManagement.IpHelper.FreeMibTable", - "Windows.Win32.NetworkManagement.IpHelper.GetIpInterfaceTable", - "Windows.Win32.NetworkManagement.IpHelper.GetUnicastIpAddressTable", - "Windows.Win32.NetworkManagement.IpHelper.MIB_IPINTERFACE_ROW", - "Windows.Win32.NetworkManagement.IpHelper.MIB_IPINTERFACE_TABLE", - "Windows.Win32.NetworkManagement.IpHelper.MIB_UNICASTIPADDRESS_ROW", - "Windows.Win32.NetworkManagement.IpHelper.MIB_UNICASTIPADDRESS_TABLE", - "Windows.Win32.NetworkManagement.IpHelper.if_indextoname", - "Windows.Win32.Networking.WinSock.AF_INET", - "Windows.Win32.Networking.WinSock.AF_INET6", - "Windows.Win32.Networking.WinSock.AF_UNSPEC", - ]) - .unwrap(); - - // Check the output is the same as before. - // Depending on the git configuration the file may have been checked out with `\r\n` newlines or - // with `\n`. Compare line-by-line to ignore this difference. - let new = fs::read_to_string(TARGET).unwrap(); - if !new.lines().eq(existing.lines()) { - println!("{new}"); - panic!("generated file `{TARGET}` is changed"); - } -} - -const TARGET: &str = "src/win_bindings.rs";