From 991bbbf8d08d5d014b8bf05bd247f841c1dc9d83 Mon Sep 17 00:00:00 2001 From: extrowerk Date: Wed, 30 Sep 2026 05:43:03 +0000 Subject: [PATCH 1/4] Initial platform support for Illumos --- src/interface/flags.rs | 3 + src/interface/ipv6_addr_flags.rs | 3 + src/interface/mod.rs | 4 ++ src/interface/mtu.rs | 3 + src/interface/state.rs | 4 ++ src/interface/types.rs | 6 +- src/os/illumos/flags.rs | 12 ++++ src/os/illumos/interface.rs | 17 +++++ src/os/illumos/ipv6_addr_flags.rs | 40 ++++++++++++ src/os/illumos/lifreq.rs | 105 ++++++++++++++++++++++++++++++ src/os/illumos/mod.rs | 21 ++++++ src/os/illumos/mtu.rs | 13 ++++ src/os/illumos/state.rs | 21 ++++++ src/os/illumos/types.rs | 17 +++++ src/os/mod.rs | 3 + src/os/unix/dns.rs | 3 + src/os/unix/interface.rs | 24 ++++++- src/os/unix/sockaddr.rs | 60 ++++++++++++++++- src/os/unix/types.rs | 3 + src/stats/counters.rs | 11 ++++ 20 files changed, 368 insertions(+), 5 deletions(-) create mode 100644 src/os/illumos/flags.rs create mode 100644 src/os/illumos/interface.rs create mode 100644 src/os/illumos/ipv6_addr_flags.rs create mode 100644 src/os/illumos/lifreq.rs create mode 100644 src/os/illumos/mod.rs create mode 100644 src/os/illumos/mtu.rs create mode 100644 src/os/illumos/state.rs create mode 100644 src/os/illumos/types.rs diff --git a/src/interface/flags.rs b/src/interface/flags.rs index 0f9b83f..f8cda2d 100644 --- a/src/interface/flags.rs +++ b/src/interface/flags.rs @@ -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::*; diff --git a/src/interface/ipv6_addr_flags.rs b/src/interface/ipv6_addr_flags.rs index 680d76b..7a10028 100644 --- a/src/interface/ipv6_addr_flags.rs +++ b/src/interface/ipv6_addr_flags.rs @@ -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"))] diff --git a/src/interface/mod.rs b/src/interface/mod.rs index e4d1ecd..2152940 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -111,6 +111,10 @@ pub(crate) fn interfaces() -> Vec { { crate::os::bsd::interface::interfaces() } + #[cfg(target_os = "illumos")] + { + crate::os::illumos::interface::interfaces() + } } #[cfg(test)] diff --git a/src/interface/mtu.rs b/src/interface/mtu.rs index 33ea824..1f6a5ab 100644 --- a/src/interface/mtu.rs +++ b/src/interface/mtu.rs @@ -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::*; diff --git a/src/interface/state.rs b/src/interface/state.rs index fd2f977..03a1c76 100644 --- a/src/interface/state.rs +++ b/src/interface/state.rs @@ -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) diff --git a/src/interface/types.rs b/src/interface/types.rs index c21df4f..cd514d7 100644 --- a/src/interface/types.rs +++ b/src/interface/types.rs @@ -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 + // 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 { diff --git a/src/os/illumos/flags.rs b/src/os/illumos/flags.rs new file mode 100644 index 0000000..e544ccc --- /dev/null +++ b/src/os/illumos/flags.rs @@ -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() +} diff --git a/src/os/illumos/interface.rs b/src/os/illumos/interface.rs new file mode 100644 index 0000000..8786f1c --- /dev/null +++ b/src/os/illumos/interface.rs @@ -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 { + unix_interfaces() +} diff --git a/src/os/illumos/ipv6_addr_flags.rs b/src/os/illumos/ipv6_addr_flags.rs new file mode 100644 index 0000000..bd26d62 --- /dev/null +++ b/src/os/illumos/ipv6_addr_flags.rs @@ -0,0 +1,40 @@ +use std::net::Ipv6Addr; + +use crate::interface::ipv6_addr_flags::Ipv6AddrFlags; + +use super::lifreq::{SIOCGLIFFLAGS, get_lifreq}; + +// From / 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, + } +} diff --git a/src/os/illumos/lifreq.rs b/src/os/illumos/lifreq.rs new file mode 100644 index 0000000..661db25 --- /dev/null +++ b/src/os/illumos/lifreq.rs @@ -0,0 +1,105 @@ +//! `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; 256]` sized to match its largest member +//! (`struct sockaddr_storage`), so `size_of::()` -- 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 ``. +const LIFNAMSIZ: usize = 32; + +#[repr(C)] +union LifrLifru { + /// Stand-in for the union's largest member (`struct sockaddr_storage`), so the union + /// -- and therefore the enclosing `Lifreq` -- has the right size and alignment even + /// though we never read this variant directly. + _addr: [u8; 256], + /// `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_lifru: LifrLifru, +} + +impl Lifreq { + fn for_name(name: &str) -> Option { + 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 ``. +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::()); +/// `SIOCGLIFMTU` -- get interface MTU. `_IOWR('i', 122, struct lifreq)`. +pub(crate) const SIOCGLIFMTU: c_int = iowr(b'i', 122, size_of::()); + +/// 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 { + 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) } +} diff --git a/src/os/illumos/mod.rs b/src/os/illumos/mod.rs new file mode 100644 index 0000000..24c876f --- /dev/null +++ b/src/os/illumos/mod.rs @@ -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; diff --git a/src/os/illumos/mtu.rs b/src/os/illumos/mtu.rs new file mode 100644 index 0000000..56c50ea --- /dev/null +++ b/src/os/illumos/mtu.rs @@ -0,0 +1,13 @@ +use super::lifreq::{SIOCGLIFMTU, get_lifreq}; + +/// Reads the MTU of a logical interface via `SIOCGLIFMTU`. +/// +/// illumos doesn't attach an MTU to the `getifaddrs` entry itself (`ifa` is unused here, +/// unlike the BSD/Darwin backends which pull it out of `if_data`), so this issues its own +/// ioctl. The query is tried over an `AF_INET` socket first, then `AF_INET6`, since a +/// v6-only logical interface may not resolve over an `AF_INET` one (and vice versa). +pub(crate) fn get_mtu(_ifa: &libc::ifaddrs, name: &str) -> Option { + get_lifreq(libc::AF_INET, name, SIOCGLIFMTU) + .or_else(|| get_lifreq(libc::AF_INET6, name, SIOCGLIFMTU)) + .map(|req| unsafe { req.mtu() }) +} diff --git a/src/os/illumos/state.rs b/src/os/illumos/state.rs new file mode 100644 index 0000000..029a41d --- /dev/null +++ b/src/os/illumos/state.rs @@ -0,0 +1,21 @@ +use crate::interface::state::OperState; + +use super::lifreq::{SIOCGLIFFLAGS, get_lifreq}; + +/// Reads the current operational state of `if_name` by issuing a fresh `SIOCGLIFFLAGS` +/// and deriving state from `IFF_UP`/`IFF_RUNNING`, same as the BSD backend. +/// +/// Tries an `AF_INET` socket first, then `AF_INET6`, since a v6-only logical interface +/// name may not resolve over an `AF_INET` one. +pub fn operstate(if_name: &str) -> OperState { + let flags = get_lifreq(libc::AF_INET, if_name, SIOCGLIFFLAGS) + .or_else(|| get_lifreq(libc::AF_INET6, if_name, SIOCGLIFFLAGS)) + .map(|req| unsafe { req.flags() }); + + match flags { + // Only the low-order IFF_UP / IFF_RUNNING bits matter to `from_if_flags`, both of + // which fit safely in the truncated u32. + Some(f) => OperState::from_if_flags(f as u32), + None => OperState::Unknown, + } +} diff --git a/src/os/illumos/types.rs b/src/os/illumos/types.rs new file mode 100644 index 0000000..17f1071 --- /dev/null +++ b/src/os/illumos/types.rs @@ -0,0 +1,17 @@ +use crate::interface::types::InterfaceType; + +/// Best-effort interface type classification for illumos. +/// +/// Unlike the BSD family, illumos' `getifaddrs` doesn't attach a BSD-style `if_data` blob +/// carrying a media/type code to the link-layer (`AF_LINK`) entry, and there's no single +/// cheap userland call that returns one uniformly across drivers (`e1000g`, `igb`, +/// `ixgbe`, `vioif`, `vnic`, link aggregations, ...). Loopback is reported precisely from +/// the interface flags already read via `getifaddrs`; everything else is reported as +/// `Ethernet`, which is correct for the large majority of illumos NICs and virtual NICs. +pub fn get_interface_type(addr_ref: &libc::ifaddrs) -> InterfaceType { + if addr_ref.ifa_flags & (libc::IFF_LOOPBACK as u64) != 0 { + InterfaceType::Loopback + } else { + InterfaceType::Ethernet + } +} diff --git a/src/os/mod.rs b/src/os/mod.rs index b840c49..60c8d05 100644 --- a/src/os/mod.rs +++ b/src/os/mod.rs @@ -21,3 +21,6 @@ pub mod windows; #[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] pub mod bsd; + +#[cfg(target_os = "illumos")] +pub mod illumos; diff --git a/src/os/unix/dns.rs b/src/os/unix/dns.rs index 105b7f0..e2452d5 100644 --- a/src/os/unix/dns.rs +++ b/src/os/unix/dns.rs @@ -3,6 +3,9 @@ use std::net::IpAddr; use std::net::ToSocketAddrs; #[cfg_attr(target_os = "ios", allow(dead_code))] +// Not yet called on illumos: DNS/gateway discovery isn't implemented there (see +// crate::os::illumos::interface). +#[cfg_attr(target_os = "illumos", allow(dead_code))] pub fn get_system_dns_conf() -> Vec { const PATH_RESOLV_CONF: &str = "/etc/resolv.conf"; let r = read_to_string(PATH_RESOLV_CONF); diff --git a/src/os/unix/interface.rs b/src/os/unix/interface.rs index 5b3fd69..c83d8a5 100644 --- a/src/os/unix/interface.rs +++ b/src/os/unix/interface.rs @@ -12,6 +12,26 @@ use crate::ipnet::{Ipv4Net, Ipv6Net}; use crate::os::unix::types::{get_interface_type, interface_name_from_ptr}; use crate::stats::counters::{InterfaceStats, get_stats}; +// `libc::ifaddrs::ifa_flags` is `u64` on illumos but `u32` on every other unix target this +// crate supports (Linux, Android, Apple, the BSDs). `Interface::flags` and +// `OperState::from_if_flags` are both `u32`, so this normalizes at the one call site below +// rather than widening those public types for one platform. Truncation only drops +// illumos's extended (>31-bit) flag bits (e.g. `IFF_TEMPORARY`, `IFF_DUPLICATE`), which +// `Interface::flags` was never able to represent on any platform anyway; illumos code that +// needs those reads full 64-bit flags directly via `SIOCGLIFFLAGS` instead (see +// `crate::os::illumos::ipv6_addr_flags` and `crate::os::illumos::state`). +#[cfg(target_os = "illumos")] +#[inline] +fn ifa_flags_u32(f: u64) -> u32 { + f as u32 +} + +#[cfg(not(target_os = "illumos"))] +#[inline] +fn ifa_flags_u32(f: u32) -> u32 { + f +} + #[cfg(target_os = "android")] pub fn unix_interfaces() -> Vec { use crate::os::android; @@ -160,8 +180,8 @@ fn unix_interfaces_inner( None => Vec::new(), }, ipv6_addr_flags: ini_ipv6_flags, - flags: addr_ref.ifa_flags, - oper_state: OperState::from_if_flags(addr_ref.ifa_flags), + flags: ifa_flags_u32(addr_ref.ifa_flags), + oper_state: OperState::from_if_flags(ifa_flags_u32(addr_ref.ifa_flags)), transmit_speed: None, receive_speed: None, auto_negotiate: None, diff --git a/src/os/unix/sockaddr.rs b/src/os/unix/sockaddr.rs index 52d0641..64df3cc 100644 --- a/src/os/unix/sockaddr.rs +++ b/src/os/unix/sockaddr.rs @@ -279,6 +279,50 @@ pub(crate) unsafe fn try_mac_from_raw( } } +// illumos has `AF_LINK`/`sockaddr_dl` like the BSD family, but its `sockaddr` has no +// `sa_len` field, and `libc`'s `sockaddr_dl` for this target is a proper named struct +// rather than an opaque byte blob, so this reads the fields directly instead of doing +// BSD's raw offset arithmetic above. +#[cfg(target_os = "illumos")] +pub(crate) unsafe fn try_mac_from_raw( + sa: *const libc::sockaddr, + len: libc::socklen_t, +) -> Option { + if sa.is_null() || (len as usize) < core::mem::size_of::() { + return None; + } + + let family = unsafe { (*sa).sa_family as libc::c_int }; + if family != libc::AF_LINK { + return None; + } + if (len as usize) < core::mem::size_of::() { + return None; + } + + let sdl = unsafe { &*(sa as *const libc::sockaddr_dl) }; + let nlen = sdl.sdl_nlen as usize; + let alen = sdl.sdl_alen as usize; + if alen < 6 { + return None; + } + // `sdl_data` holds the interface name (`sdl_nlen` bytes) immediately followed by the + // link-layer address (`sdl_alen` bytes). + if nlen + 6 > sdl.sdl_data.len() { + return None; + } + + let octet = |i: usize| sdl.sdl_data[nlen + i] as u8; + Some(MacAddr::from_octets([ + octet(0), + octet(1), + octet(2), + octet(3), + octet(4), + octet(5), + ])) +} + /// Computes the effective length of a `sockaddr` structure #[inline] pub(crate) unsafe fn compute_sockaddr_len( @@ -366,11 +410,23 @@ fn guess_len_from_family(family: libc::c_int) -> Option { return Some(core::mem::size_of::() as libc::socklen_t); } + // illumos: Layer 2 (AF_LINK). Its `sockaddr_dl` has no variable-length trailer to + // worry about the way BSD's comment above does -- `sdl_data` is a fixed-size array -- + // and there's no `sa_len` to read a truer length from, so the struct size is the + // whole story. + #[cfg(target_os = "illumos")] + if family == libc::AF_LINK { + return Some(core::mem::size_of::() as libc::socklen_t); + } + // Unknown or unsupported family None } -#[cfg(any(target_os = "linux", target_os = "android"))] +// illumos' `struct sockaddr` has no `sa_len` field either (it's SVR4-derived, not +// 4.4BSD-derived), so it shares this family-only implementation with Linux/Android rather +// than the sa_len-based one below. +#[cfg(any(target_os = "linux", target_os = "android", target_os = "illumos"))] #[inline] pub(crate) unsafe fn netmask_ip_autolen(sa: *const libc::sockaddr) -> Option { if sa.is_null() { @@ -461,7 +517,7 @@ pub(crate) unsafe fn netmask_ip_autolen(sa: *const libc::sockaddr) -> Option Option { if sa.is_null() { diff --git a/src/os/unix/types.rs b/src/os/unix/types.rs index f9f743e..ddf2953 100644 --- a/src/os/unix/types.rs +++ b/src/os/unix/types.rs @@ -29,3 +29,6 @@ pub use crate::os::darwin::types::get_interface_type; #[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] pub use crate::os::bsd::types::get_interface_type; + +#[cfg(target_os = "illumos")] +pub use crate::os::illumos::types::get_interface_type; diff --git a/src/stats/counters.rs b/src/stats/counters.rs index 4d5623a..bba8f2c 100644 --- a/src/stats/counters.rs +++ b/src/stats/counters.rs @@ -68,6 +68,14 @@ pub(crate) fn get_stats(_ifa: Option<&libc::ifaddrs>, name: &str) -> Option" kstat chain via /dev/kstat), which hasn't been ported yet. +#[cfg(target_os = "illumos")] +pub(crate) fn get_stats(_ifa: Option<&libc::ifaddrs>, _name: &str) -> Option { + None +} + #[cfg(target_os = "linux")] pub(crate) fn get_stats_from_name(name: &str) -> Option { use std::fs::read_to_string; @@ -212,6 +220,9 @@ pub(crate) fn get_stats_from_index(index: u32) -> Option { } } +// On illumos, none of the branches below touch `iface` yet (no kstat integration), so it's +// otherwise unused there. +#[cfg_attr(target_os = "illumos", allow(unused_variables))] pub(crate) fn update_interface_stats(iface: &mut Interface) -> std::io::Result<()> { #[cfg(any(target_os = "linux", target_os = "android"))] { From 7e0ac1372b3f1bc95ca5da4415e2463025012a2b Mon Sep 17 00:00:00 2001 From: extrowerk Date: Tue, 20 Oct 2026 18:44:39 +0000 Subject: [PATCH 2/4] illumos: fix lifreq layout (add lifr_type, size the union for lif_nd_req) --- src/os/illumos/lifreq.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/os/illumos/lifreq.rs b/src/os/illumos/lifreq.rs index 661db25..d1d1313 100644 --- a/src/os/illumos/lifreq.rs +++ b/src/os/illumos/lifreq.rs @@ -6,8 +6,8 @@ //! `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; 256]` sized to match its largest member -//! (`struct sockaddr_storage`), so `size_of::()` -- which feeds directly into the +//! rest of `lifr_lifru` is represented as a `[u8; 336]` sized to match its largest member +//! (`struct lif_nd_req`), so `size_of::()` -- which feeds directly into the //! ioctl command numbers below, per illumos' `_IOWR` macro -- matches what the kernel //! expects. //! @@ -24,10 +24,10 @@ const LIFNAMSIZ: usize = 32; #[repr(C)] union LifrLifru { - /// Stand-in for the union's largest member (`struct sockaddr_storage`), so the union + /// 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; 256], + _addr: [u8; 336], /// `SIOC[GS]LIFFLAGS`. flags: u64, /// `SIOC[GS]LIFMTU`. @@ -40,6 +40,8 @@ pub(crate) struct Lifreq { /// `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, } @@ -103,3 +105,16 @@ pub(crate) fn get_lifreq(af: c_int, name: &str, cmd: c_int) -> Option { 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 . + assert_eq!(size_of::(), 376); + assert_eq!(SIOCGLIFFLAGS, 0xc078_6975u32 as c_int); + assert_eq!(SIOCGLIFMTU, 0xc078_697au32 as c_int); + } +} From 7490bf7142a3a8319297c4f938f44e476f5b4e9b Mon Sep 17 00:00:00 2001 From: extrowerk Date: Thu, 22 Oct 2026 21:22:02 +0000 Subject: [PATCH 3/4] ci: run tests on illumos in an OmniOS VM --- .github/workflows/illumos.yaml | 122 +++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 .github/workflows/illumos.yaml diff --git a/.github/workflows/illumos.yaml b/.github/workflows/illumos.yaml new file mode 100644 index 0000000..187fd25 --- /dev/null +++ b/.github/workflows/illumos.yaml @@ -0,0 +1,122 @@ +# 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 + --locked + --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 From 1f9deb96f21aa74363d358245f4782a8758e9ad5 Mon Sep 17 00:00:00 2001 From: extrowerk Date: Sat, 24 Oct 2026 06:16:09 +0000 Subject: [PATCH 4/4] ci: do not pass --locked, netdev does not track Cargo.lock --- .github/workflows/illumos.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/illumos.yaml b/.github/workflows/illumos.yaml index 187fd25..004c16a 100644 --- a/.github/workflows/illumos.yaml +++ b/.github/workflows/illumos.yaml @@ -68,7 +68,6 @@ jobs: - name: build test archive run: > cargo nextest archive - --locked --all-features --lib --bins --tests --target "$RUST_TARGET"