Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .github/workflows/illumos.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# illumos tests in an OmniOS VM (vmactions/omnios-vm).
#
# The build happens on the Linux host inside the cross-rs illumos image
# (sccache via the GHA cache): `cargo nextest archive` packs the test
# binaries, and the OmniOS VM only needs the standalone nextest binary to
# run them -- no Rust toolchain in the VM.

name: illumos

on:
pull_request:
merge_group:
push:
branches:
- main
workflow_dispatch:

concurrency:
group: illumos-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
RUST_BACKTRACE: 1
RUSTFLAGS: -Dwarnings
RUST_TARGET: x86_64-unknown-illumos
# Builds the archive (taiki-e/install-action below) and runs it in the VM
# (prebuilt illumos binary). Keep both on the same version.
NEXTEST_VERSION: "0.9.80"
# Plain image: the VM only runs prebuilt binaries. Switch to
# r151058-build if anything in `prepare` turns out to need a compiler.
OMNIOS_RELEASE: r151058

# Default to read-only; jobs that need more grant it explicitly.
permissions:
contents: read

jobs:
build:
name: illumos cross build
timeout-minutes: 30
runs-on: ubuntu-latest
container:
# Provides the illumos sysroot and gcc.
image: ghcr.io/cross-rs/x86_64-unknown-illumos:main@sha256:cef932f2e49ca7ee4fce3c62ad8bdf069eacea016467e55404d807d8b130bb0f
env:
RUSTC_WRAPPER: "sccache"
SCCACHE_GHA_ENABLED: "on"
# The image also sets this (plus CC/AR) in its ENV; repeated here with
# an absolute path so the job doesn't depend on the image ENV
# surviving into the job container.
CARGO_TARGET_X86_64_UNKNOWN_ILLUMOS_LINKER: /usr/local/x86_64-unknown-illumos/bin/x86_64-unknown-illumos-gcc
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # v1
with:
toolchain: stable
targets: x86_64-unknown-illumos

- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11

- uses: taiki-e/install-action@f7e5d7c961414b23f5b25b2da9294395d08513ad # v2.87.26
with:
tool: nextest@0.9.80

- name: build test archive
run: >
cargo nextest archive
--all-features
--lib --bins --tests
--target "$RUST_TARGET"
--archive-file illumos-tests.tar.zst

- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: illumos-tests
path: illumos-tests.tar.zst
retention-days: 1
compression-level: 0

test:
name: illumos test (OmniOS VM)
needs: build
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
# The archive is remapped onto this checkout, for tests that read files
# from the source tree.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: illumos-tests

- name: run tests in OmniOS
uses: vmactions/omnios-vm@9576f0b6d15e7696548409baa533bc0a339caf42 # v1.3.7
env:
RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG' }}
with:
release: ${{ env.OMNIOS_RELEASE }}
envs: 'RUST_BACKTRACE RUST_LOG NEXTEST_VERSION'
# Nothing to copy back out of the VM.
copyback: false
prepare: |
set -e
mkdir -p /usr/local/bin
# illumos tar can't decompress from stdin, hence gunzip
# (https://www.illumos.org/issues/15228).
curl -LsSf "https://get.nexte.st/$NEXTEST_VERSION/illumos" | gunzip | tar xf - -C /usr/local/bin
run: |
set -e
uname -a
psrinfo | wc -l
/usr/local/bin/cargo-nextest nextest run \
--archive-file illumos-tests.tar.zst \
--workspace-remap "$PWD" \
--no-fail-fast
3 changes: 3 additions & 0 deletions src/interface/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ pub use crate::os::windows::flags::*;

#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))]
pub use crate::os::bsd::flags::*;

#[cfg(target_os = "illumos")]
pub use crate::os::illumos::flags::*;
3 changes: 3 additions & 0 deletions src/interface/ipv6_addr_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub(crate) use crate::os::darwin::ipv6_addr_flags::*;
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))]
pub(crate) use crate::os::bsd::ipv6_addr_flags::*;

#[cfg(target_os = "illumos")]
pub(crate) use crate::os::illumos::ipv6_addr_flags::*;

// On Linux/Android flags come from netlink; this is only reached via the
// `unix_interfaces()` fallback when netlink is unavailable.
#[cfg(any(target_os = "linux", target_os = "android"))]
Expand Down
4 changes: 4 additions & 0 deletions src/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ pub(crate) fn interfaces() -> Vec<Interface> {
{
crate::os::bsd::interface::interfaces()
}
#[cfg(target_os = "illumos")]
{
crate::os::illumos::interface::interfaces()
}
}

#[cfg(test)]
Expand Down
3 changes: 3 additions & 0 deletions src/interface/mtu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ pub(crate) use crate::os::darwin::mtu::*;

#[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))]
pub(crate) use crate::os::bsd::mtu::*;

#[cfg(target_os = "illumos")]
pub(crate) use crate::os::illumos::mtu::*;
4 changes: 4 additions & 0 deletions src/interface/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ pub fn operstate(if_name: &str) -> OperState {
{
crate::os::bsd::state::operstate(if_name)
}
#[cfg(target_os = "illumos")]
{
crate::os::illumos::state::operstate(if_name)
}
#[cfg(target_os = "windows")]
{
crate::os::windows::state::operstate(if_name)
Expand Down
6 changes: 5 additions & 1 deletion src/interface/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,15 @@ impl InterfaceType {
///
/// For variants that have no direct mapping on the current platform, this method returns
/// `u32::MAX`.
// These are the IANA-standard SNMP ifType numbers (RFC1573,
// iana.org/assignments/smi-numbers), not a BSD-specific scheme, so illumos' own
// <net/if_types.h> uses the same values for the entries below.
#[cfg(any(
target_vendor = "apple",
target_os = "openbsd",
target_os = "freebsd",
target_os = "netbsd"
target_os = "netbsd",
target_os = "illumos"
))]
pub fn value(&self) -> u32 {
match *self {
Expand Down
12 changes: 12 additions & 0 deletions src/os/illumos/flags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::interface::interface::Interface;

/// Best-effort physical-interface heuristic for illumos.
///
/// Mirrors the BSD heuristic: up, running, not a tunnel-like point-to-point interface,
/// and not loopback. illumos additionally has VNICs, IPMP interfaces, and other virtual
/// constructs that aren't distinguished by a dedicated flag bit readable from userland, so
/// those may still pass this check; `Interface::is_physical()` additionally filters by MAC
/// vendor (via `crate::net::db::oui`), which catches most of them.
pub fn is_physical_interface(interface: &Interface) -> bool {
interface.is_up() && interface.is_running() && !interface.is_tun() && !interface.is_loopback()
}
17 changes: 17 additions & 0 deletions src/os/illumos/interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use crate::{interface::interface::Interface, os::unix::interface::unix_interfaces};

/// Enumerates network interfaces on illumos-based systems.
///
/// This delegates entirely to the shared `getifaddrs`-based path in
/// `crate::os::unix::interface`; per-address MAC, netmask, MTU and IPv6 flag lookups are
/// filled in from there via the platform hooks in this module.
///
/// `Interface::gateway`, `Interface::dns_servers` and `Interface::default` are left at
/// their defaults here even when the `gateway` feature is enabled -- default-route
/// discovery on illumos needs a `PF_ROUTE` routing-socket query that hasn't been ported
/// yet, so `netdev::get_default_gateway()` will return `Err` until that lands.
/// `netdev::get_default_interface()` still works, since it falls back to matching the
/// locally-routed source IP against each interface's addresses.
pub fn interfaces() -> Vec<Interface> {
unix_interfaces()
}
40 changes: 40 additions & 0 deletions src/os/illumos/ipv6_addr_flags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::net::Ipv6Addr;

use crate::interface::ipv6_addr_flags::Ipv6AddrFlags;

use super::lifreq::{SIOCGLIFFLAGS, get_lifreq};

// From <sys/sockio.h> / if_tcp(7P); not yet exposed by `libc` for illumos. Unlike the BSD
// family (which has a dedicated `SIOCGIFAFLAG_IN6` ioctl for per-address state), illumos
// folds address-state bits directly into the owning logical interface's own extended
// flags, since each address normally lives on its own logical interface
// (`net0`, `net0:1`, `net0:2`, ...).
const IFF_DEPRECATED: u64 = 0x0000040000;
const IFF_TEMPORARY: u64 = 0x0800000000;
const IFF_DUPLICATE: u64 = 0x4000000000;

/// Reads IPv6 address-state flags for the logical interface named `ifname` via
/// `SIOCGLIFFLAGS`.
///
/// `ifname` is expected to be the exact logical interface that carries `addr` (as passed
/// down from `getifaddrs` in `crate::os::unix::interface::unix_interfaces`); `addr` itself
/// isn't needed for the lookup, since illumos keys these flags by interface name rather
/// than by address.
pub(crate) fn get_ipv6_addr_flags(ifname: &str, _addr: &Ipv6Addr) -> Ipv6AddrFlags {
let flags = get_lifreq(libc::AF_INET6, ifname, SIOCGLIFFLAGS)
.map(|req| unsafe { req.flags() })
.unwrap_or(0);

Ipv6AddrFlags {
deprecated: flags & IFF_DEPRECATED != 0,
temporary: flags & IFF_TEMPORARY != 0,
// illumos doesn't expose a distinct "tentative" (DAD in progress) bit through
// SIOCGLIFFLAGS -- addresses simply aren't reported by getifaddrs until DAD
// completes -- so this is always `false` here.
tentative: false,
duplicated: flags & IFF_DUPLICATE != 0,
// IFF_DEPRECATED / IFF_TEMPORARY / IFF_DUPLICATE are the only address-state bits
// illumos exposes this way; there's no equivalent of Linux's IFA_F_PERMANENT.
permanent: false,
}
}
120 changes: 120 additions & 0 deletions src/os/illumos/lifreq.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! `struct lifreq` and the `SIOCG*LIF*` ioctl numbers used to query per-logical-interface
//! flags and MTU on illumos.
//!
//! `libc` does not currently bind `struct lifreq` or these ioctls for `target_os =
//! "illumos"`, so both are reproduced here from the illumos-gate headers:
//! `usr/src/uts/common/sys/sockio.h` (ioctl numbers, as `_IOWR('i', num, struct lifreq)`)
//! and `usr/src/uts/common/sys/if.h` / `if_tcp(7P)` (the `lifreq` layout). Only the union
//! members this crate actually reads (`lifru_flags`, `lifru_mtu`) are given real names; the
//! rest of `lifr_lifru` is represented as a `[u8; 336]` sized to match its largest member
//! (`struct lif_nd_req`), so `size_of::<Lifreq>()` -- which feeds directly into the
//! ioctl command numbers below, per illumos' `_IOWR` macro -- matches what the kernel
//! expects.
//!
//! Note: unlike every other `libc` unix target, illumos declares
//! `ioctl(fildes: c_int, request: c_int, ...)` -- the request code is a plain `c_int`, not
//! a `c_ulong`.

use std::ffi::CString;
use std::mem::size_of;
use std::os::raw::{c_char, c_int};

/// `LIFNAMSIZ` from `<net/if.h>`.
const LIFNAMSIZ: usize = 32;

#[repr(C)]
union LifrLifru {
/// Stand-in for the union's largest member (`struct lif_nd_req`), so the union
/// -- and therefore the enclosing `Lifreq` -- has the right size and alignment even
/// though we never read this variant directly.
_addr: [u8; 336],
/// `SIOC[GS]LIFFLAGS`.
flags: u64,
/// `SIOC[GS]LIFMTU`.
mtu: u32,
}

#[repr(C)]
pub(crate) struct Lifreq {
lifr_name: [c_char; LIFNAMSIZ],
/// `lifr_lifru1` union (`lifru_addrlen` / `lifru_ppa`); unused by the ioctls we issue,
/// kept only so later fields land at the right offset.
_lifr_lifru1: c_int,
/// `lifr_type` (`IFT_ETHER`, ...); unused, kept so the union lands at offset 40.
_lifr_type: u32,
lifr_lifru: LifrLifru,
}

impl Lifreq {
fn for_name(name: &str) -> Option<Self> {
let cname = CString::new(name).ok()?;
let bytes = cname.as_bytes_with_nul();
if bytes.len() > LIFNAMSIZ {
return None;
}
// All-zero is a valid bit pattern for every field here (a zeroed union is simply
// interpreted as `flags: 0` / `mtu: 0` until the kernel fills it in).
let mut req: Self = unsafe { std::mem::zeroed() };
for (dst, src) in req.lifr_name.iter_mut().zip(bytes.iter()) {
*dst = *src as c_char;
}
Some(req)
}

/// # Safety
/// Only valid to call after a successful `SIOCGLIFFLAGS` ioctl populated this request.
pub(crate) unsafe fn flags(&self) -> u64 {
unsafe { self.lifr_lifru.flags }
}

/// # Safety
/// Only valid to call after a successful `SIOCGLIFMTU` ioctl populated this request.
pub(crate) unsafe fn mtu(&self) -> u32 {
unsafe { self.lifr_lifru.mtu }
}
}

const IOC_INOUT: u32 = 0xC000_0000;
const IOCPARM_MASK: u32 = 0xff;

/// Mirrors illumos' `_IOWR(group, num, t)` macro from `<sys/ioccom.h>`.
const fn iowr(group: u8, num: u8, size: usize) -> c_int {
(IOC_INOUT | (((size as u32) & IOCPARM_MASK) << 16) | ((group as u32) << 8) | (num as u32))
as c_int
}

/// `SIOCGLIFFLAGS` -- get extended (64-bit) interface flags. `_IOWR('i', 117, struct lifreq)`.
pub(crate) const SIOCGLIFFLAGS: c_int = iowr(b'i', 117, size_of::<Lifreq>());
/// `SIOCGLIFMTU` -- get interface MTU. `_IOWR('i', 122, struct lifreq)`.
pub(crate) const SIOCGLIFMTU: c_int = iowr(b'i', 122, size_of::<Lifreq>());

/// Opens a throwaway datagram socket in address family `af`, issues `cmd` (one of the
/// `SIOCG*` constants above) against the named logical interface, and returns the
/// populated request on success.
///
/// `af` should match the address family of the logical interface being queried (`AF_INET`
/// for an IPv4-only name, `AF_INET6` for an IPv6-only one); callers that aren't sure can
/// try both.
pub(crate) fn get_lifreq(af: c_int, name: &str, cmd: c_int) -> Option<Lifreq> {
let mut req = Lifreq::for_name(name)?;
let fd = unsafe { libc::socket(af, libc::SOCK_DGRAM, 0) };
if fd < 0 {
return None;
}
let res = unsafe { libc::ioctl(fd, cmd, &mut req as *mut Lifreq) };
unsafe { libc::close(fd) };
if res < 0 { None } else { Some(req) }
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn layout_matches_illumos_header() {
// sizeof(struct lifreq) == 376 on LP64 illumos, per <net/if.h>.
assert_eq!(size_of::<Lifreq>(), 376);
assert_eq!(SIOCGLIFFLAGS, 0xc078_6975u32 as c_int);
assert_eq!(SIOCGLIFMTU, 0xc078_697au32 as c_int);
}
}
21 changes: 21 additions & 0 deletions src/os/illumos/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! illumos / Solaris-derivative backend (illumos, OmniOS, SmartOS, ...).
//!
//! Interface enumeration reuses the shared `getifaddrs`-based path in
//! `crate::os::unix::interface`. The pieces that differ from the BSD family enough to need
//! their own implementation live here: `struct lifreq`-based MTU and flag queries (`libc`
//! does not currently expose `lifreq` or the `SIOC*LIF*` ioctls for this target), and an
//! interface-type guess that doesn't depend on a BSD-style `if_data` blob.
//!
//! Not yet implemented: default-gateway / DNS discovery (`Interface::gateway`,
//! `Interface::dns_servers`, `Interface::default` stay at their defaults even with the
//! `gateway` feature enabled) and native traffic counters (`Interface::stats` stays `None`).
//! Both would need a routing-socket (`PF_ROUTE`) or `kstat(3KSTAT)` integration that hasn't
//! been ported yet.

pub mod flags;
pub mod interface;
pub mod ipv6_addr_flags;
mod lifreq;
pub mod mtu;
pub mod state;
pub mod types;
Loading
Loading