diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 615ff32..bada2fb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -294,6 +294,33 @@ jobs: - name: clippy check (default features) run: cargo clippy --workspace --all-targets + external_types: + timeout-minutes: 30 + name: Check external types + runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "on" + # Pin to the nightly that the pinned `cargo-check-external-types` + # release was last tested against. Update both together, and keep the + # toolchain in sync with the `check-external-types` task in Makefile.toml. + CARGO_CHECK_EXTERNAL_TYPES_VERSION: "0.4.0" + TOOLCHAIN: "nightly-2025-10-18" + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.TOOLCHAIN }} + - name: Install sccache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@main + - uses: taiki-e/install-action@cargo-make + - name: Install cargo-check-external-types + run: cargo binstall cargo-check-external-types@${{ env.CARGO_CHECK_EXTERNAL_TYPES_VERSION }} --locked --no-confirm + - name: Check external types + run: cargo make check-external-types + msrv: if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')" timeout-minutes: 30 diff --git a/Makefile.toml b/Makefile.toml index da5115c..c540d74 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -18,6 +18,17 @@ workspace = false command = "cargo" args = ["nextest", "run", "-p", "netwatch", "--test", "patchbay", "--profile", "patchbay", "${@}"] +# Verifies netwatch does not leak unapproved external crate types (notably +# `netdev`) through its public API. The allowlist lives in +# `netwatch/Cargo.toml` under `[package.metadata.cargo_check_external_types]`. +# Requires the nightly toolchain that the pinned `cargo-check-external-types` +# release was tested against; keep both in sync with CI. +[tasks.check-external-types] +workspace = false +toolchain = "${TOOLCHAIN:nightly-2025-10-18}" +command = "cargo" +args = ["check-external-types", "--manifest-path", "netwatch/Cargo.toml"] + [tasks.format-check] workspace = false command = "cargo" diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index 2396af4..e2164a6 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -18,6 +18,9 @@ workspace = true [dependencies] atomic-waker = "1.1.2" bytes = "1.7" +# ipnet is pure arithmetic and builds everywhere, including wasm and esp-idf; +# the `IpNet` public type uses it on every platform. +ipnet = "2" n0-error = "=1.0.0-rc.0" n0-future = "0.3.1" n0-watcher = "=1.0.0-rc.0" @@ -34,7 +37,6 @@ tracing = "0.1" # non-browser dependencies [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] -ipnet = "2" noq-udp = "=1.0.0-rc.1" libc = "0.2.139" socket2 = { version = "0.6", features = ["all"] } @@ -108,3 +110,24 @@ cfg_aliases = { version = "0.2.1" } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "iroh_docsrs"] + +# Types from these external crates are allowed to appear in netwatch's public +# API. The list is enforced by `cargo check-external-types` (see the +# `check-external-types` task in Makefile.toml). Notably absent is `netdev`: +# its types must never leak, which is why the `interfaces` module mirrors them. +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + # IP network types used by `interfaces::IpNet`. + "ipnet::*", + # iroh-ecosystem error and reactive-state crates. + "n0_error::*", + "n0_watcher::*", + # UDP transmit/receive metadata used by the `udp` module. + "noq_udp::*", + # Error types surfaced by `From` impls on `netmon::Error`. + "tokio::sync::mpsc::error::SendError", + "tokio::sync::oneshot::error::RecvError", + # `State::last_unsuspend` is an `n0_future::time::Instant`, which resolves + # to tokio's `Instant` on non-wasm targets. + "tokio::time::instant::Instant", +] diff --git a/netwatch/build.rs b/netwatch/build.rs index e4f9a69..404d474 100644 --- a/netwatch/build.rs +++ b/netwatch/build.rs @@ -7,5 +7,11 @@ fn main() { wasm_browser: { all(target_family = "wasm", target_os = "unknown") }, // Limited POSIX platforms (not wasm) posix_minimal: { target_os = "espidf" }, + // Platforms where the `netdev` crate is available, i.e. everything + // except esp-idf and wasm-in-browser. Keep in sync with the `netdev` + // dependency target gate in Cargo.toml. + netdev: { not(any(target_os = "espidf", all(target_family = "wasm", target_os = "unknown"))) }, + // BSD-derived platforms that share the `AF_ROUTE` routing-socket code. + bsd: { any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "macos", target_os = "ios") }, } } diff --git a/netwatch/src/interfaces.rs b/netwatch/src/interfaces.rs index 7c90584..ecbce5d 100644 --- a/netwatch/src/interfaces.rs +++ b/netwatch/src/interfaces.rs @@ -1,140 +1,77 @@ //! Contains helpers for looking up system network interfaces. +//! +//! All public types are defined here once and have the same shape on every +//! platform. The platform-specific work (enumerating interfaces, finding the +//! default route, locating the home router) lives in the submodules below and +//! is reached through the cfg-selected `platform` alias. Conversion from the +//! `netdev` crate is confined to the `netdev_impl` module. use std::{collections::HashMap, fmt, net::IpAddr}; +pub(crate) use ipnet::{Ipv4Net, Ipv6Net}; use n0_future::time::Instant; -#[cfg(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "macos", - target_os = "ios" -))] +use crate::ip::{LocalAddresses, is_link_local}; + +// Each platform module provides the same three entry points, reached through +// the `platform` alias: `get_state()`, `default_route()` and `home_router()`. +// The `netdev`-capable modules share enumeration via `netdev_impl`. +#[cfg(netdev)] +mod netdev_impl; + +#[cfg(bsd)] pub(super) mod bsd; #[cfg(any(target_os = "linux", target_os = "android"))] mod linux; +#[cfg(posix_minimal)] +mod posix_minimal; +#[cfg(wasm_browser)] +mod wasm_browser; #[cfg(target_os = "windows")] mod windows; -pub(crate) use ipnet::{Ipv4Net, Ipv6Net}; -pub use netdev::interface::ipv6_addr_flags::Ipv6AddrFlags; - -#[cfg(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "macos", - target_os = "ios" -))] -use self::bsd::default_route; +#[cfg(bsd)] +use self::bsd as platform; #[cfg(any(target_os = "linux", target_os = "android"))] -use self::linux::default_route; +use self::linux as platform; +#[cfg(posix_minimal)] +use self::posix_minimal as platform; +#[cfg(wasm_browser)] +use self::wasm_browser as platform; #[cfg(target_os = "windows")] -use self::windows::default_route; -#[cfg(not(wasm_browser))] -use crate::ip::is_link_local; -use crate::ip::{LocalAddresses, is_private_v6, is_up}; -#[cfg(not(wasm_browser))] -use crate::netmon::is_interesting_interface; - -/// Represents a network interface. -#[derive(Debug, Clone)] -pub struct Interface { - iface: netdev::Interface, -} - -impl fmt::Display for Interface { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}. {} {:?} ipv4={:?} ipv6={:?}", - self.iface.index, self.iface.name, self.iface.if_type, self.iface.ipv4, self.iface.ipv6 - ) - } -} - -impl PartialEq for Interface { - fn eq(&self, other: &Self) -> bool { - self.iface.index == other.iface.index - && self.iface.name == other.iface.name - && self.iface.flags == other.iface.flags - && self.iface.mac_addr.as_ref().map(|a| a.octets()) - == other.iface.mac_addr.as_ref().map(|a| a.octets()) - } -} - -impl Eq for Interface {} - -impl Interface { - /// Is this interface up? - pub fn is_up(&self) -> bool { - is_up(&self.iface) - } - - /// The name of the interface. - pub fn name(&self) -> &str { - &self.iface.name - } - - /// A list of all ip addresses of this interface. - pub fn addrs(&self) -> impl Iterator + '_ { - self.iface.ipv4.iter().cloned().map(IpNet::V4).chain( - self.iface - .ipv6 - .iter() - .zip(self.iface.ipv6_scope_ids.iter()) - .zip(self.iface.ipv6_addr_flags.iter()) - .map(|((net, scope_id), flags)| IpNet::V6 { - net: *net, - scope_id: *scope_id, - flags: *flags, - }), - ) - } +use self::windows as platform; - /// Creates a fake interface for usage in tests. - /// - /// This allows tests to be independent of the host interfaces. - pub(crate) fn fake() -> Self { - use std::net::Ipv4Addr; - - use netdev::{ - NetworkDevice, - prelude::{InterfaceType, MacAddr, OperState}, - }; +/// The interface flag bit indicating that an interface is up. +/// +/// Matches the POSIX `IFF_UP` value. On platforms that do not expose BSD-style +/// interface flags it is synthesized from the platform's notion of "up". +const IFF_UP: u32 = 0x1; - Self { - iface: netdev::Interface { - index: 2, - name: String::from("wifi0"), - friendly_name: None, - description: None, - if_type: InterfaceType::Ethernet, - mac_addr: Some(MacAddr::new(2, 3, 4, 5, 6, 7)), - ipv4: vec![Ipv4Net::new(Ipv4Addr::new(192, 168, 0, 189), 24).unwrap()], - ipv6: vec![], - flags: 69699, - transmit_speed: None, - receive_speed: None, - gateway: Some(NetworkDevice { - mac_addr: MacAddr::new(2, 3, 4, 5, 6, 8), - ipv4: vec![Ipv4Addr::from([192, 168, 0, 1])], - ipv6: vec![], - }), - dns_servers: vec![], - default: false, - ipv6_scope_ids: vec![], - ipv6_addr_flags: vec![], - stats: None, - mtu: None, - oper_state: OperState::Up, - auto_negotiate: None, - dhcp_v4_enabled: None, - dhcp_v6_enabled: None, - }, - } - } +/// State flags for a single IPv6 address. +/// +/// Hand-kept mirror of netdev's `Ipv6AddrFlags`, so the `interfaces` API is +/// identical on platforms built without `netdev` (e.g. esp-idf). All fields +/// default to `false` when the platform does not provide the information. +/// +/// Flags are collected from platform-specific sources: +/// +/// - **Linux/Android**: netlink `IFA_FLAGS` attribute (`IFA_F_*` from [``]) +/// - **macOS/iOS/FreeBSD/OpenBSD/NetBSD**: `SIOCGIFAFLAG_IN6` ioctl (`IN6_IFF_*`) +/// - **Windows**: `NL_DAD_STATE` and `NL_SUFFIX_ORIGIN` from `IP_ADAPTER_UNICAST_ADDRESS` +/// +/// [``]: https://github.com/torvalds/linux/blob/master/include/uapi/linux/if_addr.h +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub struct Ipv6AddrFlags { + /// Preferred lifetime expired; should not be used for new connections. + pub deprecated: bool, + /// Privacy address ([RFC 4941](https://datatracker.ietf.org/doc/html/rfc4941)). + pub temporary: bool, + /// Undergoing duplicate address detection. + pub tentative: bool, + /// Duplicate address detection failed. + pub duplicated: bool, + /// Manually configured, not from SLAAC. + pub permanent: bool, } /// Structure of an IP network, either IPv4 or IPv6. @@ -146,7 +83,7 @@ pub enum IpNet { V6 { /// The actual network address. net: Ipv6Net, - /// IPv6 scope ID + /// IPv6 scope ID. scope_id: u32, /// IPv6 address flags. flags: Ipv6AddrFlags, @@ -195,11 +132,81 @@ impl IpNet { } } +/// Represents a network interface. +#[derive(Debug, Clone)] +pub struct Interface { + name: String, + index: u32, + /// BSD-style interface flags, or a synthesized value carrying only + /// [`IFF_UP`] on platforms without real flags. + flags: u32, + mac_addr: Option<[u8; 6]>, + addrs: Vec, +} + +impl fmt::Display for Interface { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}. {} up={} addrs={:?}", + self.index, + self.name, + self.is_up(), + self.addrs + ) + } +} + +impl PartialEq for Interface { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + && self.name == other.name + && self.flags == other.flags + && self.mac_addr == other.mac_addr + } +} + +impl Eq for Interface {} + +impl Interface { + /// Is this interface up? + pub fn is_up(&self) -> bool { + self.flags & IFF_UP != 0 + } + + /// The name of the interface. + pub fn name(&self) -> &str { + &self.name + } + + /// A list of all ip addresses of this interface. + pub fn addrs(&self) -> impl Iterator + '_ { + self.addrs.iter().cloned() + } + + /// Creates a fake interface for usage in tests. + /// + /// This allows tests to be independent of the host interfaces. + pub(crate) fn fake() -> Self { + use std::net::Ipv4Addr; + + Self { + name: String::from("wifi0"), + index: 2, + flags: 69699, + mac_addr: Some([2, 3, 4, 5, 6, 7]), + addrs: vec![IpNet::V4( + Ipv4Net::new(Ipv4Addr::new(192, 168, 0, 189), 24).unwrap(), + )], + } + } +} + /// Intended to store the state of the machine's network interfaces, routing table, and /// other network configuration. For now it's pretty basic. #[derive(Debug, PartialEq, Eq, Clone)] pub struct State { - /// Maps from an interface name interface. + /// Maps from an interface name to the interface. pub interfaces: HashMap, /// List of machine's local IP addresses. pub local_addresses: LocalAddresses, @@ -212,14 +219,14 @@ pub struct State { pub have_v4: bool, /// Whether the current network interface is considered "expensive", which currently means LTE/etc - /// instead of Wifi. This field is not populated by `get_state`. + /// instead of Wifi. This field is not populated by `State::new`. pub is_expensive: bool, /// The interface name for the machine's default route. /// /// It is not yet populated on all OSes. /// - /// When set, its value is the map key into `interface` and `interface_ips`. + /// When set, its value is the map key into `interfaces`. pub default_route_interface: Option, /// Monotonic timestamp, when an unsuspend was detected. @@ -229,7 +236,7 @@ pub struct State { impl fmt::Display for State { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut ifaces: Vec<_> = self.interfaces.values().collect(); - ifaces.sort_by_key(|iface| iface.iface.index); + ifaces.sort_by_key(|iface| iface.index); for iface in ifaces { write!(f, "{iface}")?; if let Some(ref default_if) = self.default_route_interface @@ -250,52 +257,9 @@ impl fmt::Display for State { impl State { /// Returns the state of all the current machine's network interfaces. /// - /// It does not set the returned `State.is_expensive`. The caller can populate that. + /// It does not set the returned `State::is_expensive`. The caller can populate that. pub async fn new() -> Self { - let mut interfaces = HashMap::new(); - let mut have_v6 = false; - let mut have_v4 = false; - - let ifaces = netdev::interface::get_interfaces(); - let local_addresses = LocalAddresses::from_raw_interfaces(&ifaces); - - for mut iface in ifaces { - // ensure these are all sorted, so any comparisons made are stable - iface.ipv4.sort(); - iface.dns_servers.sort(); - - // ipv6_scope_ids and ipv6_addr_flags need to match ipv6 order - sort_ipv6(&mut iface); - - let ni = Interface { iface }; - let if_up = ni.is_up(); - let name = ni.iface.name.clone(); - let pfxs: Vec<_> = ni.addrs().collect(); - - if if_up { - for pfx in &pfxs { - if pfx.addr().is_loopback() { - continue; - } - have_v6 |= is_usable_v6(&pfx.addr()); - have_v4 |= is_usable_v4(&pfx.addr()); - } - } - - interfaces.insert(name, ni); - } - - let default_route_interface = default_route_interface().await; - - State { - interfaces, - local_addresses, - have_v4, - have_v6, - is_expensive: false, - default_route_interface, - last_unsuspend: None, - } + platform::get_state().await } /// Creates a fake interface state for usage in tests. @@ -303,10 +267,10 @@ impl State { /// This allows tests to be independent of the host interfaces. pub fn fake() -> Self { let fake = Interface::fake(); - let ifname = fake.iface.name.clone(); + let ifname = fake.name().to_string(); Self { interfaces: [(ifname.clone(), fake)].into_iter().collect(), - local_addresses: LocalAddresses::from_raw_interfaces(&[]), + local_addresses: LocalAddresses::default(), have_v6: true, have_v4: true, is_expensive: false, @@ -351,61 +315,22 @@ impl State { } } -/// Sorts all ipv6 related fields in one go. -fn sort_ipv6(iface: &mut netdev::Interface) { - let n = iface.ipv6.len(); - debug_assert_eq!(n, iface.ipv6_scope_ids.len()); - debug_assert_eq!(n, iface.ipv6_addr_flags.len()); - - let mut keys: Vec<_> = (0..n).collect(); - - // calculate ordering - keys.sort_by_key(|x| &iface.ipv6[*x]); - - // apply ordering to all three - iface.ipv6 = keys.iter().map(|&i| iface.ipv6[i]).collect(); - iface.ipv6_scope_ids = keys.iter().map(|&i| iface.ipv6_scope_ids[i]).collect(); - iface.ipv6_addr_flags = keys.iter().map(|&i| iface.ipv6_addr_flags[i]).collect(); -} - -/// Reports whether ip is a usable IPv4 address which should have Internet connectivity. +/// Reports whether the interface named `name` is one whose changes we care +/// about when deciding if the network state changed in a major way. /// -/// Globally routable and private IPv4 addresses are always Usable, and link local -/// 169.254.x.x addresses are in some environments. -fn is_usable_v4(ip: &IpAddr) -> bool { - if !ip.is_ipv4() || ip.is_loopback() { - return false; - } - - true -} - -/// Reports whether ip is a usable IPv6 address which should have Internet connectivity. -/// -/// Globally routable IPv6 addresses are always Usable, and Unique Local Addresses -/// (fc00::/7) are in some environments used with address translation. -/// -/// We consider all 2000::/3 addresses to be routable, which is the interpretation of -/// -/// as well. However this probably includes some addresses which should not be routed, -/// e.g. documentation addresses. See also -/// for an -/// alternative implementation which is both stricter and laxer in some regards. -fn is_usable_v6(ip: &IpAddr) -> bool { - match ip { - IpAddr::V6(ip) => { - // V6 Global1 2000::/3 - let mask: u16 = 0b1110_0000_0000_0000; - let base: u16 = 0x2000; - let segment1 = ip.segments()[0]; - if (base & mask) == (segment1 & mask) { - return true; - } - - is_private_v6(ip) +/// Most platforms consider every interface interesting. Apple platforms hide a +/// few virtual interfaces (AWDL, low-latency Wi-Fi, IPsec) whose churn would +/// otherwise generate spurious change events. +pub(crate) fn is_interesting_interface(name: &str) -> bool { + #[cfg(bsd)] + { + let base_name = name.trim_end_matches("0123456789"); + if base_name == "llw" || base_name == "awdl" || base_name == "ipsec" { + return false; } - IpAddr::V4(_) => false, } + let _ = name; + true } /// The details about a default route. @@ -419,16 +344,16 @@ pub struct DefaultRouteDetails { impl DefaultRouteDetails { /// Reads the default route from the current system and returns the details. pub async fn new() -> Option { - default_route().await + platform::default_route().await } } -/// Like `DefaultRoutDetails::new` but only returns the interface name. +/// Like `DefaultRouteDetails::new` but only returns the interface name. pub async fn default_route_interface() -> Option { DefaultRouteDetails::new().await.map(|v| v.interface_name) } -/// Likely IPs of the residentla router, and the ip address of the current +/// Likely IPs of the residential router, and the ip address of the current /// machine using it. #[derive(Debug, Clone)] pub struct HomeRouter { @@ -445,41 +370,12 @@ impl HomeRouter { /// the LAN using that gateway. /// This is used as the destination for UPnP, NAT-PMP, PCP, etc queries. pub fn new() -> Option { - let gateway = Self::get_default_gateway()?; - let my_ip = netdev::net::ip::get_local_ipaddr(); - - Some(HomeRouter { gateway, my_ip }) - } - - #[cfg(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "macos", - target_os = "ios" - ))] - fn get_default_gateway() -> Option { - // netdev doesn't work yet - // See: https://github.com/shellrow/default-net/issues/34 - bsd::likely_home_router() - } - - #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] - fn get_default_gateway() -> Option { - let gateway = netdev::get_default_gateway().ok()?; - gateway - .ipv4 - .iter() - .cloned() - .map(IpAddr::V4) - .chain(gateway.ipv6.iter().cloned().map(IpAddr::V6)) - .next() + platform::home_router() } } /// Checks whether `a` and `b` are equal after ignoring uninteresting /// things, like link-local, loopback and multicast addresses. -#[cfg(not(wasm_browser))] fn prefixes_major_equal(a: impl Iterator, b: impl Iterator) -> bool { fn is_interesting(p: &IpNet) -> bool { let a = p.addr(); @@ -503,8 +399,6 @@ fn prefixes_major_equal(a: impl Iterator, b: impl Iterator Option { }) } -pub fn likely_home_router() -> Option { +/// Locates the home router via the routing table. +/// +/// `netdev` cannot yet determine the default gateway on BSD platforms (see +/// ), so this parses the +/// routing table directly. The local IP still comes from `netdev`. +pub(super) fn home_router() -> Option { + let gateway = likely_home_router()?; + Some(HomeRouter { + gateway, + my_ip: super::netdev_impl::local_ip(), + }) +} + +fn likely_home_router() -> Option { let rib = fetch_routing_table()?; let msgs = parse_routing_table(&rib)?; for rm in msgs { diff --git a/netwatch/src/interfaces/linux.rs b/netwatch/src/interfaces/linux.rs index 0dce28a..8b6bbff 100644 --- a/netwatch/src/interfaces/linux.rs +++ b/netwatch/src/interfaces/linux.rs @@ -7,6 +7,7 @@ use tokio::{ }; use super::DefaultRouteDetails; +pub(super) use super::netdev_impl::{get_state, home_router}; #[stack_error(derive, add_meta, from_sources, std_sources)] #[non_exhaustive] diff --git a/netwatch/src/interfaces/netdev_impl.rs b/netwatch/src/interfaces/netdev_impl.rs new file mode 100644 index 0000000..7980a13 --- /dev/null +++ b/netwatch/src/interfaces/netdev_impl.rs @@ -0,0 +1,291 @@ +//! Conversion from the `netdev` crate into our platform-agnostic interface +//! types, plus the shared interface enumeration and home-router lookup used by +//! all `netdev`-capable platforms (linux, android, bsd, macos, windows). +//! +//! This is the only module that depends on `netdev`. Everything it produces is +//! expressed in terms of the types defined in [`crate::interfaces`]. + +use std::net::IpAddr; + +use super::{Interface, IpNet, Ipv6AddrFlags, State}; +use crate::ip::{LocalAddresses, is_link_local, is_private, is_private_v6}; + +const IFF_UP: u32 = 0x1; +const IFF_LOOPBACK: u32 = 0x8; + +/// Converts netdev's IPv6 address flags into our mirrored [`Ipv6AddrFlags`]. +/// +/// This is a free function rather than a `From` impl on purpose: a public +/// `From` would re-expose the `netdev` type in our public API, +/// which is exactly what the [`Ipv6AddrFlags`] mirror exists to avoid. +fn to_ipv6_addr_flags(flags: netdev::interface::ipv6_addr_flags::Ipv6AddrFlags) -> Ipv6AddrFlags { + Ipv6AddrFlags { + deprecated: flags.deprecated, + temporary: flags.temporary, + tentative: flags.tentative, + duplicated: flags.duplicated, + permanent: flags.permanent, + } +} + +/// Converts a [`netdev::Interface`] into our platform-agnostic [`Interface`]. +/// +/// Addresses are sorted (IPv4 first, then IPv6, each by address) so that +/// comparisons between successive snapshots are stable. +fn to_interface(iface: netdev::Interface) -> Interface { + // netdev keeps these three IPv6 arrays parallel, one entry per address. + // The zip below relies on that; assert it so a netdev change that breaks + // the invariant surfaces in tests rather than silently dropping addresses. + debug_assert_eq!(iface.ipv6.len(), iface.ipv6_scope_ids.len()); + debug_assert_eq!(iface.ipv6.len(), iface.ipv6_addr_flags.len()); + + let mut v4: Vec = iface.ipv4.iter().copied().map(IpNet::V4).collect(); + let mut v6: Vec = iface + .ipv6 + .iter() + .copied() + .zip(iface.ipv6_scope_ids.iter().copied()) + .zip(iface.ipv6_addr_flags.iter().copied()) + .map(|((net, scope_id), flags)| IpNet::V6 { + net, + scope_id, + flags: to_ipv6_addr_flags(flags), + }) + .collect(); + + // Sort each family by address so successive snapshots compare equal, then + // concatenate as IPv4-first. + v4.sort_by_key(IpNet::addr); + v6.sort_by_key(IpNet::addr); + let mut addrs = v4; + addrs.append(&mut v6); + + Interface { + name: iface.name, + index: iface.index, + flags: iface.flags, + mac_addr: iface.mac_addr.as_ref().map(|a| a.octets()), + addrs, + } +} + +/// Enumerates the machine's network interfaces and assembles the [`State`]. +pub(super) async fn get_state() -> State { + let raw = netdev::interface::get_interfaces(); + let local_addresses = local_addresses(&raw); + + let mut interfaces = std::collections::HashMap::new(); + let mut have_v6 = false; + let mut have_v4 = false; + + for raw in raw { + let iface = to_interface(raw); + if iface.is_up() { + for pfx in iface.addrs() { + let addr = pfx.addr(); + if addr.is_loopback() { + continue; + } + have_v6 |= is_usable_v6(&addr); + have_v4 |= is_usable_v4(&addr); + } + } + interfaces.insert(iface.name().to_string(), iface); + } + + let default_route_interface = super::default_route_interface().await; + + State { + interfaces, + local_addresses, + have_v4, + have_v6, + is_expensive: false, + default_route_interface, + last_unsuspend: None, + } +} + +/// The shared home-router lookup for linux, android and windows. +/// +/// BSD platforms do not use this; `netdev` cannot yet determine their default +/// gateway, so [`super::bsd`] provides its own implementation. +#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] +pub(super) fn home_router() -> Option { + let gateway = netdev::get_default_gateway().ok()?; + let gateway = gateway + .ipv4 + .iter() + .copied() + .map(IpAddr::V4) + .chain(gateway.ipv6.iter().copied().map(IpAddr::V6)) + .next()?; + + Some(super::HomeRouter { + gateway, + my_ip: local_ip(), + }) +} + +/// Reports whether `ip` is a usable IPv4 address which should have Internet connectivity. +/// +/// Globally routable and private IPv4 addresses are always usable, and link-local +/// 169.254.x.x addresses are in some environments. +fn is_usable_v4(ip: &IpAddr) -> bool { + if !ip.is_ipv4() || ip.is_loopback() { + return false; + } + + true +} + +/// Reports whether `ip` is a usable IPv6 address which should have Internet connectivity. +/// +/// Globally routable IPv6 addresses are always usable, and Unique Local Addresses +/// (fc00::/7) are in some environments used with address translation. +/// +/// We consider all 2000::/3 addresses to be routable, which is the interpretation of +/// +/// as well. However this probably includes some addresses which should not be routed, +/// e.g. documentation addresses. See also +/// for an +/// alternative implementation which is both stricter and laxer in some regards. +fn is_usable_v6(ip: &IpAddr) -> bool { + match ip { + IpAddr::V6(ip) => { + // V6 Global1 2000::/3 + let mask: u16 = 0b1110_0000_0000_0000; + let base: u16 = 0x2000; + let segment1 = ip.segments()[0]; + if (base & mask) == (segment1 & mask) { + return true; + } + + is_private_v6(ip) + } + IpAddr::V4(_) => false, + } +} + +/// The local IP address of this machine, as reported by `netdev`. +pub(super) fn local_ip() -> Option { + netdev::net::ip::get_local_ipaddr() +} + +const fn is_up(interface: &netdev::Interface) -> bool { + interface.flags & IFF_UP != 0 +} + +const fn is_loopback(interface: &netdev::Interface) -> bool { + interface.flags & IFF_LOOPBACK != 0 +} + +/// Builds the machine's [`LocalAddresses`] from a raw `netdev` interface list. +/// +/// If there are no regular addresses it falls back to IPv4 link-local or IPv6 +/// unique-local addresses, because we know of environments where these are used +/// with NAT to provide connectivity. +fn local_addresses(ifaces: &[netdev::Interface]) -> LocalAddresses { + let mut loopback = Vec::new(); + let mut regular4 = Vec::new(); + let mut regular6 = Vec::new(); + let mut linklocal4 = Vec::new(); + let mut ula6 = Vec::new(); + + for iface in ifaces { + if !is_up(iface) { + // Skip down interfaces + continue; + } + let ifc_is_loopback = is_loopback(iface); + let addrs = iface + .ipv4 + .iter() + .map(|a| IpAddr::V4(a.addr())) + .chain(iface.ipv6.iter().map(|a| IpAddr::V6(a.addr()))); + + for ip in addrs { + let ip = ip.to_canonical(); + + if ip.is_loopback() || ifc_is_loopback { + loopback.push(ip); + } else if is_link_local(ip) { + if ip.is_ipv4() { + linklocal4.push(ip); + } + + // We know of no cases where the IPv6 fe80:: addresses + // are used to provide WAN connectivity. It is also very + // common for users to have no IPv6 WAN connectivity, + // but their OS supports IPv6 so they have an fe80:: + // address. We don't want to report all of those + // IPv6 LL to Control. + } else if ip.is_ipv6() && is_private(&ip) { + // Google Cloud Run uses NAT with IPv6 Unique + // Local Addresses to provide IPv6 connectivity. + ula6.push(ip); + } else if ip.is_ipv4() { + regular4.push(ip); + } else { + regular6.push(ip); + } + } + } + + if regular4.is_empty() && regular6.is_empty() { + // if we have no usable IP addresses then be willing to accept + // addresses we otherwise wouldn't, like: + // + 169.254.x.x (AWS Lambda uses NAT with these) + // + IPv6 ULA (Google Cloud Run uses these with address translation) + regular4 = linklocal4; + regular6 = ula6; + } + let mut regular = regular4; + regular.extend(regular6); + + regular.sort(); + loopback.sort(); + + LocalAddresses { loopback, regular } +} + +impl LocalAddresses { + /// Returns the machine's IP addresses. + /// + /// If there are no regular addresses it will return any IPv4 link-local or + /// IPv6 unique-local addresses, because we know of environments where these + /// are used with NAT to provide connectivity. + pub fn new() -> Self { + local_addresses(&netdev::interface::get_interfaces()) + } +} + +#[cfg(test)] +mod tests { + use std::net::Ipv6Addr; + + use super::*; + + #[test] + fn test_local_addresses() { + let addrs = LocalAddresses::new(); + dbg!(&addrs); + assert!(!addrs.loopback.is_empty()); + assert!(!addrs.regular.is_empty()); + } + + #[test] + fn test_is_usable_v6() { + let loopback = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0x1); + assert!(!is_usable_v6(&loopback.into())); + + let link_local = Ipv6Addr::new(0xfe80, 0, 0, 0, 0xcbc9, 0x6aff, 0x5b07, 0x4a9e); + assert!(!is_usable_v6(&link_local.into())); + + let relay_use1 = Ipv6Addr::new(0x2a01, 0x4ff, 0xf0, 0xc4a1, 0, 0, 0, 0x1); + assert!(is_usable_v6(&relay_use1.into())); + + let random_2603 = Ipv6Addr::new(0x2603, 0x3ff, 0xf1, 0xc3aa, 0x1, 0x2, 0x3, 0x1); + assert!(is_usable_v6(&random_2603.into())); + } +} diff --git a/netwatch/src/interfaces/posix_minimal.rs b/netwatch/src/interfaces/posix_minimal.rs index d3e2439..0631cca 100644 --- a/netwatch/src/interfaces/posix_minimal.rs +++ b/netwatch/src/interfaces/posix_minimal.rs @@ -1,167 +1,28 @@ -//! Fallback interfaces implementation for platforms without `netdev`. -//! Provides stub types — no network interface enumeration available. +//! Fallback interface enumeration for POSIX platforms without `netdev` +//! (e.g. esp-idf). No interface enumeration, default route, or home router is +//! available, so these report empty or absent. -use std::{collections::HashMap, fmt, net::IpAddr}; - -pub(crate) use ipnet::{Ipv4Net, Ipv6Net}; -use n0_future::time::Instant; +use std::collections::HashMap; +use super::{DefaultRouteDetails, HomeRouter, State}; use crate::ip::LocalAddresses; -/// Represents a network interface (stub). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Interface; - -impl fmt::Display for Interface { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "unknown") - } -} - -impl Interface { - /// A list of all ip addresses of this interface. - pub fn addrs(&self) -> impl Iterator + '_ { - std::iter::empty() - } -} - -/// State flags for a single IPv6 address. -/// -/// Hand-kept mirror of netdev's `Ipv6AddrFlags`, so the `interfaces` API is -/// identical on platforms built without `netdev` (e.g. esp-idf). All fields -/// default to `false` when the platform does not provide the information. -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] -pub struct Ipv6AddrFlags { - /// Preferred lifetime expired; should not be used for new connections. - pub deprecated: bool, - /// Privacy address ([RFC 4941](https://datatracker.ietf.org/doc/html/rfc4941)). - pub temporary: bool, - /// Undergoing duplicate address detection. - pub tentative: bool, - /// Duplicate address detection failed. - pub duplicated: bool, - /// Manually configured, not from SLAAC. - pub permanent: bool, -} - -/// Structure of an IP network, either IPv4 or IPv6. -/// -/// The shape mirrors the `netdev`-based `IpNet` so downstream code is -/// platform-agnostic. -#[derive(Clone, Debug)] -pub enum IpNet { - /// Structure of IPv4 Network. - V4(Ipv4Net), - /// Structure of IPv6 Network. - V6 { - /// The actual network address. - net: Ipv6Net, - /// IPv6 scope ID - scope_id: u32, - /// IPv6 address flags. - flags: Ipv6AddrFlags, - }, -} - -impl PartialEq for IpNet { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (IpNet::V4(a), IpNet::V4(b)) => { - a.addr() == b.addr() - && a.prefix_len() == b.prefix_len() - && a.netmask() == b.netmask() - } - ( - IpNet::V6 { - net: net_a, - scope_id: scope_id_a, - flags: flags_a, - }, - IpNet::V6 { - net: net_b, - scope_id: scope_id_b, - flags: flags_b, - }, - ) => { - net_a.addr() == net_b.addr() - && net_a.prefix_len() == net_b.prefix_len() - && net_a.netmask() == net_b.netmask() - && scope_id_a == scope_id_b - && flags_a == flags_b - } - _ => false, - } - } -} -impl Eq for IpNet {} - -impl IpNet { - /// The IP address of this structure. - pub fn addr(&self) -> IpAddr { - match self { - IpNet::V4(a) => IpAddr::V4(a.addr()), - IpNet::V6 { net, .. } => IpAddr::V6(net.addr()), - } - } -} - -/// The router/gateway of the local network (stub — always returns `None`). -#[derive(Debug, Clone)] -pub struct HomeRouter { - /// Ip of the router. - pub gateway: IpAddr, - /// Our local Ip if known. - pub my_ip: Option, -} - -impl HomeRouter { - /// Returns `None` — no gateway discovery available on this platform. - pub fn new() -> Option { - None +pub(super) async fn get_state() -> State { + State { + interfaces: HashMap::new(), + local_addresses: LocalAddresses::default(), + have_v6: false, + have_v4: true, + is_expensive: false, + default_route_interface: None, + last_unsuspend: None, } } -/// Intended to store the state of the machine's network interfaces. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct State { - /// Maps from an interface name to interface. - pub interfaces: HashMap, - /// List of machine's local IP addresses. - pub local_addresses: LocalAddresses, - /// Whether this machine has IPv6 connectivity. - pub have_v6: bool, - /// Whether the machine has IPv4 connectivity. - pub have_v4: bool, - /// Whether the current network interface is considered "expensive". - pub(crate) is_expensive: bool, - /// The interface name for the machine's default route. - pub default_route_interface: Option, - /// Monotonic timestamp, when an unsuspend was detected. - pub last_unsuspend: Option, +pub(super) async fn default_route() -> Option { + None } -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "fallback(no interfaces)") - } -} - -impl State { - /// Returns a default empty state (no interface enumeration on this platform). - pub async fn new() -> Self { - State { - interfaces: HashMap::new(), - local_addresses: LocalAddresses::default(), - have_v6: false, - have_v4: true, - is_expensive: false, - default_route_interface: None, - last_unsuspend: None, - } - } - - /// Is this a major change compared to the `old` one? - pub fn is_major_change(&self, old: &State) -> bool { - self != old - } +pub(super) fn home_router() -> Option { + None } diff --git a/netwatch/src/interfaces/wasm_browser.rs b/netwatch/src/interfaces/wasm_browser.rs index 782bb65..cb18553 100644 --- a/netwatch/src/interfaces/wasm_browser.rs +++ b/netwatch/src/interfaces/wasm_browser.rs @@ -1,134 +1,69 @@ -use std::{collections::HashMap, fmt}; +//! Browser (wasm) interface enumeration. +//! +//! Browsers expose a single bit of connectivity information, +//! `navigator.onLine`. We model it as one synthetic interface and never have a +//! default route or home router to report. use js_sys::{JsString, Reflect}; -use n0_future::time::Instant; -pub const BROWSER_INTERFACE: &str = "browserif"; +use super::{DefaultRouteDetails, HomeRouter, IFF_UP, Interface, State}; +use crate::ip::LocalAddresses; -/// Represents a network interface. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Interface { - is_up: bool, -} - -impl fmt::Display for Interface { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "navigator.onLine={}", self.is_up) - } -} +/// The name of the single synthetic interface we report in the browser. +pub(crate) const BROWSER_INTERFACE: &str = "browserif"; -impl Interface { - async fn new() -> Self { - let is_up = match Self::is_up() { - Some(v) => v, - None => { - tracing::warn!("navigator.onLine unavailable, assuming up"); - true - } - }; - tracing::debug!(onLine = is_up, "Fetched globalThis.navigator.onLine"); - Self { is_up } - } - - fn is_up() -> Option { +/// Reads `globalThis.navigator.onLine`, defaulting to `true` when unavailable. +fn navigator_online() -> bool { + fn read() -> Option { let navigator = Reflect::get( js_sys::global().as_ref(), JsString::from("navigator").as_ref(), ) .ok()?; - - let is_up = Reflect::get(&navigator, JsString::from("onLine").as_ref()).ok()?; - - is_up.as_bool() + let online = Reflect::get(&navigator, JsString::from("onLine").as_ref()).ok()?; + online.as_bool() } - /// The name of the interface. - pub(crate) fn name(&self) -> &str { - BROWSER_INTERFACE + match read() { + Some(v) => v, + None => { + tracing::warn!("navigator.onLine unavailable, assuming up"); + true + } } } -/// Intended to store the state of the machine's network interfaces, routing table, and -/// other network configuration. For now it's pretty basic. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct State { - /// Maps from an interface name interface. - pub interfaces: HashMap, - - /// Whether this machine has an IPv6 Global or Unique Local Address - /// which might provide connectivity. - pub have_v6: bool, - - /// Whether the machine has some non-localhost, non-link-local IPv4 address. - pub have_v4: bool, - - //// Whether the current network interface is considered "expensive", which currently means LTE/etc - /// instead of Wifi. This field is not populated by `get_state`. - pub(crate) is_expensive: bool, - - /// The interface name for the machine's default route. - /// - /// It is not yet populated on all OSes. - /// - /// When set, its value is the map key into `interface` and `interface_ips`. - pub(crate) default_route_interface: Option, - - /// The HTTP proxy to use, if any. - pub(crate) http_proxy: Option, - - /// The URL to the Proxy Autoconfig URL, if applicable. - pub(crate) pac: Option, - - /// Monotonic timestamp, when an unsuspend was detected. - pub last_unsuspend: Option, -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for iface in self.interfaces.values() { - write!(f, "{iface}")?; - if let Some(ref default_if) = self.default_route_interface { - if iface.name() == default_if { - write!(f, " (default)")?; - } - } - if f.alternate() { - writeln!(f)?; - } else { - write!(f, "; ")?; - } - } - Ok(()) +pub(super) async fn get_state() -> State { + let is_up = navigator_online(); + tracing::debug!(onLine = is_up, "Fetched globalThis.navigator.onLine"); + + let iface = Interface { + name: BROWSER_INTERFACE.to_string(), + index: 0, + flags: if is_up { IFF_UP } else { 0 }, + mac_addr: None, + addrs: Vec::new(), + }; + + State { + interfaces: [(BROWSER_INTERFACE.to_string(), iface)] + .into_iter() + .collect(), + local_addresses: LocalAddresses::default(), + have_v6: false, + have_v4: false, + is_expensive: false, + default_route_interface: Some(BROWSER_INTERFACE.to_string()), + last_unsuspend: None, } } -impl State { - /// Returns the state of all the current machine's network interfaces. - /// - /// It does not set the returned `State.is_expensive`. The caller can populate that. - pub async fn new() -> Self { - let mut interfaces = HashMap::new(); - let have_v6 = false; - let have_v4 = false; - - interfaces.insert(BROWSER_INTERFACE.to_string(), Interface::new().await); - - State { - interfaces, - have_v4, - have_v6, - is_expensive: false, - default_route_interface: Some(BROWSER_INTERFACE.to_string()), - http_proxy: None, - pac: None, - last_unsuspend: None, - } - } +pub(super) async fn default_route() -> Option { + Some(DefaultRouteDetails { + interface_name: BROWSER_INTERFACE.to_string(), + }) +} - /// Is this a major change compared to the `old` one?. - pub fn is_major_change(&self, old: &State) -> bool { - // All changes are major. - // In the browser, there only are changes from online to offline - self != old - } +pub(super) fn home_router() -> Option { + None } diff --git a/netwatch/src/interfaces/windows.rs b/netwatch/src/interfaces/windows.rs index 4fc2ce1..ed89c58 100644 --- a/netwatch/src/interfaces/windows.rs +++ b/netwatch/src/interfaces/windows.rs @@ -6,6 +6,7 @@ use tracing::warn; use wmi::{FilterValue, WMIConnection}; use super::DefaultRouteDetails; +pub(super) use super::netdev_impl::{get_state, home_router}; /// API Docs: #[derive(Deserialize, Debug)] diff --git a/netwatch/src/ip.rs b/netwatch/src/ip.rs index 2580aae..234ccf3 100644 --- a/netwatch/src/ip.rs +++ b/netwatch/src/ip.rs @@ -1,17 +1,12 @@ //! IP address related utilities. -#[cfg(not(wasm_browser))] -use std::net::IpAddr; -use std::net::Ipv6Addr; - -#[cfg(not(wasm_browser))] -const IFF_UP: u32 = 0x1; -#[cfg(not(wasm_browser))] -const IFF_LOOPBACK: u32 = 0x8; +use std::net::{IpAddr, Ipv6Addr}; /// List of machine's IP addresses. -#[cfg(not(wasm_browser))] -#[derive(Debug, Clone, PartialEq, Eq)] +/// +/// The netdev-based constructors live in [`crate::interfaces`]'s `netdev_impl` +/// module; on platforms without `netdev` this is only ever the empty default. +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct LocalAddresses { /// Loopback addresses. pub loopback: Vec, @@ -19,102 +14,10 @@ pub struct LocalAddresses { pub regular: Vec, } -#[cfg(not(wasm_browser))] -impl Default for LocalAddresses { - fn default() -> Self { - Self::new() - } -} - -#[cfg(not(wasm_browser))] -impl LocalAddresses { - /// Returns the machine's IP addresses. - /// If there are no regular addresses it will return any IPv4 linklocal or IPv6 unique local - /// addresses because we know of environments where these are used with NAT to provide connectivity. - pub fn new() -> Self { - let ifaces = netdev::interface::get_interfaces(); - Self::from_raw_interfaces(&ifaces) - } - - pub(crate) fn from_raw_interfaces(ifaces: &[netdev::Interface]) -> Self { - let mut loopback = Vec::new(); - let mut regular4 = Vec::new(); - let mut regular6 = Vec::new(); - let mut linklocal4 = Vec::new(); - let mut ula6 = Vec::new(); - - for iface in ifaces { - if !is_up(iface) { - // Skip down interfaces - continue; - } - let ifc_is_loopback = is_loopback(iface); - let addrs = iface - .ipv4 - .iter() - .map(|a| IpAddr::V4(a.addr())) - .chain(iface.ipv6.iter().map(|a| IpAddr::V6(a.addr()))); - - for ip in addrs { - let ip = ip.to_canonical(); - - if ip.is_loopback() || ifc_is_loopback { - loopback.push(ip); - } else if is_link_local(ip) { - if ip.is_ipv4() { - linklocal4.push(ip); - } - - // We know of no cases where the IPv6 fe80:: addresses - // are used to provide WAN connectivity. It is also very - // common for users to have no IPv6 WAN connectivity, - // but their OS supports IPv6 so they have an fe80:: - // address. We don't want to report all of those - // IPv6 LL to Control. - } else if ip.is_ipv6() && is_private(&ip) { - // Google Cloud Run uses NAT with IPv6 Unique - // Local Addresses to provide IPv6 connectivity. - ula6.push(ip); - } else if ip.is_ipv4() { - regular4.push(ip); - } else { - regular6.push(ip); - } - } - } - - if regular4.is_empty() && regular6.is_empty() { - // if we have no usable IP addresses then be willing to accept - // addresses we otherwise wouldn't, like: - // + 169.254.x.x (AWS Lambda uses NAT with these) - // + IPv6 ULA (Google Cloud Run uses these with address translation) - regular4 = linklocal4; - regular6 = ula6; - } - let mut regular = regular4; - regular.extend(regular6); - - regular.sort(); - loopback.sort(); - - LocalAddresses { loopback, regular } - } -} - -#[cfg(not(wasm_browser))] -pub(crate) const fn is_up(interface: &netdev::Interface) -> bool { - interface.flags & IFF_UP != 0 -} - -#[cfg(not(wasm_browser))] -pub(crate) const fn is_loopback(interface: &netdev::Interface) -> bool { - interface.flags & IFF_LOOPBACK != 0 -} - -/// Reports whether ip is a private address, according to RFC 1918 +/// Reports whether `ip` is a private address, according to RFC 1918 /// (IPv4 addresses) and RFC 4193 (IPv6 addresses). That is, it reports whether /// ip is in 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, or fc00::/7. -#[cfg(not(wasm_browser))] +#[cfg(netdev)] pub(crate) fn is_private(ip: &IpAddr) -> bool { match ip { IpAddr::V4(ip) => { @@ -129,14 +32,13 @@ pub(crate) fn is_private(ip: &IpAddr) -> bool { } } -#[cfg(not(wasm_browser))] +#[cfg(netdev)] pub(crate) fn is_private_v6(ip: &Ipv6Addr) -> bool { // RFC 4193 allocates fc00::/7 as the unique local unicast IPv6 address subnet. ip.octets()[0] & 0xfe == 0xfc } -#[cfg(not(wasm_browser))] -pub(super) fn is_link_local(ip: IpAddr) -> bool { +pub(crate) fn is_link_local(ip: IpAddr) -> bool { match ip { IpAddr::V4(ip) => ip.is_link_local(), IpAddr::V6(ip) => is_unicast_link_local(ip), @@ -148,15 +50,3 @@ pub(super) fn is_link_local(ip: IpAddr) -> bool { pub const fn is_unicast_link_local(addr: Ipv6Addr) -> bool { (addr.segments()[0] & 0xffc0) == 0xfe80 } - -#[cfg(test)] -mod tests { - #[cfg(not(wasm_browser))] - #[test] - fn test_local_addresses() { - let addrs = super::LocalAddresses::new(); - dbg!(&addrs); - assert!(!addrs.loopback.is_empty()); - assert!(!addrs.regular.is_empty()); - } -} diff --git a/netwatch/src/ip_posix_minimal.rs b/netwatch/src/ip_posix_minimal.rs deleted file mode 100644 index 38c5984..0000000 --- a/netwatch/src/ip_posix_minimal.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! IP address related utilities — fallback for platforms without `netdev`. - -/// List of machine's IP addresses (stub). -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct LocalAddresses { - /// Loopback addresses. - pub loopback: Vec, - /// Regular addresses. - pub regular: Vec, -} diff --git a/netwatch/src/lib.rs b/netwatch/src/lib.rs index cb93968..ff17297 100644 --- a/netwatch/src/lib.rs +++ b/netwatch/src/lib.rs @@ -1,9 +1,6 @@ //! Networking related utilities -#[cfg_attr(wasm_browser, path = "interfaces/wasm_browser.rs")] -#[cfg_attr(posix_minimal, path = "interfaces/posix_minimal.rs")] pub mod interfaces; -#[cfg_attr(posix_minimal, path = "ip_posix_minimal.rs")] pub mod ip; mod ip_family; pub mod netmon; diff --git a/netwatch/src/netmon.rs b/netwatch/src/netmon.rs index c7c9ffc..b4750d4 100644 --- a/netwatch/src/netmon.rs +++ b/netwatch/src/netmon.rs @@ -8,13 +8,7 @@ use tokio::sync::{mpsc, oneshot}; mod actor; #[cfg(target_os = "android")] mod android; -#[cfg(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "macos", - target_os = "ios" -))] +#[cfg(bsd)] mod bsd; #[cfg(target_os = "linux")] mod linux; @@ -25,8 +19,6 @@ mod wasm_browser; #[cfg(target_os = "windows")] mod windows; -#[cfg(not(any(posix_minimal, wasm_browser)))] -pub(crate) use self::actor::is_interesting_interface; use self::actor::{Actor, ActorMessage}; pub use crate::interfaces::State; diff --git a/netwatch/src/netmon/actor.rs b/netwatch/src/netmon/actor.rs index a2783df..4f1e647 100644 --- a/netwatch/src/netmon/actor.rs +++ b/netwatch/src/netmon/actor.rs @@ -2,20 +2,12 @@ use n0_future::time::{self, Duration, Instant}; use n0_watcher::Watchable; pub(super) use os::Error; use os::RouteMonitor; -#[cfg(not(any(posix_minimal, wasm_browser)))] -pub(crate) use os::is_interesting_interface; use tokio::sync::mpsc; use tracing::{debug, trace}; #[cfg(target_os = "android")] use super::android as os; -#[cfg(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "macos", - target_os = "ios" -))] +#[cfg(bsd)] use super::bsd as os; #[cfg(target_os = "linux")] use super::linux as os; diff --git a/netwatch/src/netmon/android.rs b/netwatch/src/netmon/android.rs index 9e62b46..ec3328b 100644 --- a/netwatch/src/netmon/android.rs +++ b/netwatch/src/netmon/android.rs @@ -18,7 +18,3 @@ impl RouteMonitor { Ok(RouteMonitor { _sender }) } } - -pub(crate) fn is_interesting_interface(_name: &str) -> bool { - true -} diff --git a/netwatch/src/netmon/bsd.rs b/netwatch/src/netmon/bsd.rs index 54b0ebe..fa79109 100644 --- a/netwatch/src/netmon/bsd.rs +++ b/netwatch/src/netmon/bsd.rs @@ -11,7 +11,10 @@ use tracing::{trace, warn}; use super::actor::NetworkMessage; #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] use crate::interfaces::bsd::{RTAX_DST, RTAX_IFP}; -use crate::{interfaces::bsd::WireMessage, ip::is_link_local}; +use crate::{ + interfaces::{bsd::WireMessage, is_interesting_interface}, + ip::is_link_local, +}; #[derive(Debug)] pub(super) struct RouteMonitor { @@ -126,12 +129,3 @@ pub(super) fn is_interesting_message(msg: &WireMessage) -> bool { WireMessage::InterfaceAnnounce(_) => false, } } - -pub(crate) fn is_interesting_interface(name: &str) -> bool { - let base_name = name.trim_end_matches("0123456789"); - if base_name == "llw" || base_name == "awdl" || base_name == "ipsec" { - return false; - } - - true -} diff --git a/netwatch/src/netmon/linux.rs b/netwatch/src/netmon/linux.rs index e0d16e3..a0d6d4f 100644 --- a/netwatch/src/netmon/linux.rs +++ b/netwatch/src/netmon/linux.rs @@ -206,7 +206,3 @@ impl RouteMonitor { }) } } - -pub(crate) fn is_interesting_interface(_name: &str) -> bool { - true -} diff --git a/netwatch/src/netmon/windows.rs b/netwatch/src/netmon/windows.rs index 665ba6d..f8e9c80 100644 --- a/netwatch/src/netmon/windows.rs +++ b/netwatch/src/netmon/windows.rs @@ -52,10 +52,6 @@ impl RouteMonitor { } } -pub(crate) fn is_interesting_interface(_name: &str) -> bool { - true -} - /// Manages callbacks registered with the win32 networking API. #[derive(derive_more::Debug, Default)] struct CallbackHandler {