From 13b671f78c8cfd2a987aad516cd9c49bae9f2aa6 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 4 Mar 2026 17:50:42 +0200 Subject: [PATCH 01/12] Provide fallback for platforms that do not have netdev, such as esp32 --- Cargo.lock | 1 + Cargo.toml | 2 +- netwatch/Cargo.toml | 6 +- netwatch/build.rs | 2 + netwatch/src/interfaces.rs | 2 +- netwatch/src/interfaces/fallback.rs | 132 ++++++++++++++++++++++++++++ netwatch/src/ip_fallback.rs | 21 +++++ netwatch/src/lib.rs | 2 + netwatch/src/netmon.rs | 2 + netwatch/src/netmon/actor.rs | 2 + netwatch/src/netmon/fallback.rs | 23 +++++ 11 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 netwatch/src/interfaces/fallback.rs create mode 100644 netwatch/src/ip_fallback.rs create mode 100644 netwatch/src/netmon/fallback.rs diff --git a/Cargo.lock b/Cargo.lock index 3e578fa9..e7351435 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1042,6 +1042,7 @@ dependencies = [ "bytes", "cfg_aliases", "derive_more", + "ipnet", "iroh-quinn-udp", "js-sys", "libc", diff --git a/Cargo.toml b/Cargo.toml index 208c81e5..f0b33e82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ missing_debug_implementations = "warn" # require a feature enabled when using `--cfg docsrs` which we can not # do. To enable for a crate set `#![cfg_attr(iroh_docsrs, # feature(doc_cfg))]` in the crate. -unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)"] } +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)", "cfg(has_netdev)"] } [workspace.lints.clippy] unused-async = "warn" diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index bf64c83a..4ccccce0 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [dependencies] atomic-waker = "1.1.2" bytes = "1.7" +ipnet = "2" n0-error = "0.1.2" n0-future = "0.3.1" n0-watcher = "0.6.0" @@ -36,8 +37,11 @@ tracing = "0.1" [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] quinn-udp = { package = "iroh-quinn-udp", version = "0.8" } libc = "0.2.139" -netdev = "0.40.0" socket2 = { version = "0.6", features = ["all"] } + +# netdev is only available on desktop/mobile platforms +[target.'cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios", target_os = "windows", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] +netdev = "0.40.0" tokio = { version = "1", features = [ "io-util", "macros", diff --git a/netwatch/build.rs b/netwatch/build.rs index 7aae5682..73227086 100644 --- a/netwatch/build.rs +++ b/netwatch/build.rs @@ -5,5 +5,7 @@ fn main() { cfg_aliases! { // Convenience aliases wasm_browser: { all(target_family = "wasm", target_os = "unknown") }, + // Platforms where the `netdev` crate is available + has_netdev: { any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios", target_os = "windows", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd") }, } } diff --git a/netwatch/src/interfaces.rs b/netwatch/src/interfaces.rs index 2077946b..85d2f3bc 100644 --- a/netwatch/src/interfaces.rs +++ b/netwatch/src/interfaces.rs @@ -17,7 +17,7 @@ mod linux; #[cfg(target_os = "windows")] mod windows; -pub(crate) use netdev::ipnet::{Ipv4Net, Ipv6Net}; +pub(crate) use ipnet::{Ipv4Net, Ipv6Net}; #[cfg(any( target_os = "freebsd", diff --git a/netwatch/src/interfaces/fallback.rs b/netwatch/src/interfaces/fallback.rs new file mode 100644 index 00000000..d3de3164 --- /dev/null +++ b/netwatch/src/interfaces/fallback.rs @@ -0,0 +1,132 @@ +//! Fallback interfaces implementation for platforms without `netdev`. +//! Provides stub types — no network interface enumeration available. + +use std::{collections::HashMap, fmt, net::IpAddr}; + +pub(crate) use ipnet::{Ipv4Net, Ipv6Net}; +use n0_future::time::Instant; + +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 { + /// Is this interface up? + pub fn is_up(&self) -> bool { + false + } + + /// The name of the interface. + pub fn name(&self) -> &str { + "unknown" + } + + /// A list of all ip addresses of this interface. + pub fn addrs(&self) -> impl Iterator + '_ { + std::iter::empty() + } +} + +/// Structure of an IP network, either IPv4 or IPv6. +#[derive(Clone, Debug)] +pub enum IpNet { + /// Structure of IPv4 Network. + V4(Ipv4Net), + /// Structure of IPv6 Network. + V6(Ipv6Net), +} + +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(a), IpNet::V6(b)) => { + a.addr() == b.addr() + && a.prefix_len() == b.prefix_len() + && a.netmask() == b.netmask() + } + _ => 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(a) => IpAddr::V6(a.addr()), + } + } +} + +/// 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 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, +} + +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, + } + } + + /// Creates a fake interface state for usage in tests. + pub fn fake() -> Self { + Self { + 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 + } +} diff --git a/netwatch/src/ip_fallback.rs b/netwatch/src/ip_fallback.rs new file mode 100644 index 00000000..e640ef58 --- /dev/null +++ b/netwatch/src/ip_fallback.rs @@ -0,0 +1,21 @@ +//! IP address related utilities — fallback for platforms without `netdev`. + +use std::net::Ipv6Addr; + +/// 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, +} + +pub(crate) fn is_private_v6(ip: &Ipv6Addr) -> bool { + ip.octets()[0] & 0xfe == 0xfc +} + +/// Returns true if the address is a unicast address with link-local scope, as defined in RFC 4291. +pub const fn is_unicast_link_local(addr: Ipv6Addr) -> bool { + (addr.segments()[0] & 0xffc0) == 0xfe80 +} diff --git a/netwatch/src/lib.rs b/netwatch/src/lib.rs index 5cd2c9f2..4572390c 100644 --- a/netwatch/src/lib.rs +++ b/netwatch/src/lib.rs @@ -1,7 +1,9 @@ //! Networking related utilities #[cfg_attr(wasm_browser, path = "interfaces/wasm_browser.rs")] +#[cfg_attr(not(any(has_netdev, wasm_browser)), path = "interfaces/fallback.rs")] pub mod interfaces; +#[cfg_attr(not(any(has_netdev, wasm_browser)), path = "ip_fallback.rs")] pub mod ip; mod ip_family; pub mod netmon; diff --git a/netwatch/src/netmon.rs b/netwatch/src/netmon.rs index 22cafaa5..a518b480 100644 --- a/netwatch/src/netmon.rs +++ b/netwatch/src/netmon.rs @@ -22,6 +22,8 @@ mod linux; mod wasm_browser; #[cfg(target_os = "windows")] mod windows; +#[cfg(not(any(has_netdev, wasm_browser)))] +mod fallback; #[cfg(not(wasm_browser))] pub(crate) use self::actor::is_interesting_interface; diff --git a/netwatch/src/netmon/actor.rs b/netwatch/src/netmon/actor.rs index 3ce5bf55..ac56e93e 100644 --- a/netwatch/src/netmon/actor.rs +++ b/netwatch/src/netmon/actor.rs @@ -23,6 +23,8 @@ use super::linux as os; use super::wasm_browser as os; #[cfg(target_os = "windows")] use super::windows as os; +#[cfg(not(any(has_netdev, wasm_browser)))] +use super::fallback as os; use crate::interfaces::State; /// The message sent by the OS specific monitors. diff --git a/netwatch/src/netmon/fallback.rs b/netwatch/src/netmon/fallback.rs new file mode 100644 index 00000000..d2bbcb92 --- /dev/null +++ b/netwatch/src/netmon/fallback.rs @@ -0,0 +1,23 @@ +//! Fallback netmon implementation for platforms without OS-specific route monitoring. +//! Does nothing — no route monitoring available. + +use n0_error::stack_error; +use tokio::sync::mpsc; + +use super::actor::NetworkMessage; + +#[stack_error(derive, add_meta)] +pub struct Error; + +#[derive(Debug)] +pub(super) struct RouteMonitor; + +impl RouteMonitor { + pub(super) fn new(_sender: mpsc::Sender) -> Result { + Ok(RouteMonitor) + } +} + +pub(crate) fn is_interesting_interface(_name: &str) -> bool { + true +} From 84d6c24f4c66875225f78c94be7764b695bd80e2 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 5 Mar 2026 10:56:18 +0200 Subject: [PATCH 02/12] deps(netwatch): use noq-udp instead of iroh-quinn-udp Use a git dep for now since noq is not yet published. --- netwatch/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index 049b24a8..16623c21 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -34,7 +34,7 @@ tracing = "0.1" # non-browser dependencies [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] -quinn-udp = { package = "iroh-quinn-udp", version = "0.8" } +quinn-udp = { package = "noq-udp", git = "https://github.com/n0-computer/noq", branch = "main" } libc = "0.2.139" netdev = "0.40.0" socket2 = { version = "0.6", features = ["all"] } From 36c1ecbe421bb13c51af47863c14fe52f72e768e Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 5 Mar 2026 11:40:50 +0200 Subject: [PATCH 03/12] WIP use branch that has the fallback.rs renamed and has the send fix. --- netwatch/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index 60584c45..6c6598b3 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -35,7 +35,7 @@ tracing = "0.1" # non-browser dependencies [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] -quinn-udp = { package = "noq-udp", git = "https://github.com/n0-computer/noq", branch = "main" } +quinn-udp = { package = "noq-udp", git = "https://github.com/n0-computer/noq", branch = "swallow-send-error" } libc = "0.2.139" socket2 = { version = "0.6", features = ["all"] } From 947068e92fb0bcb8898925068d8d8bf071388fea Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 10:27:32 +0200 Subject: [PATCH 04/12] Add check that net-tools compiles for esp32 Currently failing --- .github/workflows/ci.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e0ee0b8b..6714dd97 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -147,6 +147,20 @@ jobs: env: RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG' }} + esp32_check: + name: ESP32-C3 build check + if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')" + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + - uses: Swatinem/rust-cache@v2 + - name: Check netwatch for ESP32-C3 + run: cargo check -Z build-std=std,panic_abort --target riscv32imc-esp-espidf -p netwatch --no-default-features + wasm_test: name: Build & test wasm32 for browsers runs-on: ubuntu-latest From a7170580ecc0d8bab5e1508cadc0bc86f408ede0 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 11:00:20 +0200 Subject: [PATCH 05/12] Move deps around to make it compile --- netwatch/Cargo.toml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index 4609c858..e1ffbef1 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -28,6 +28,8 @@ tokio = { version = "1", features = [ "io-util", "macros", "sync", + "rt", + "net", "time", ] } tokio-util = { version = "0.7", features = ["rt"] } @@ -43,14 +45,8 @@ socket2 = { version = "0.6", features = ["all"] } [target.'cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios", target_os = "windows", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] netdev = "0.40.1" tokio = { version = "1", features = [ - "io-util", - "macros", - "sync", - "rt", - "net", "fs", "io-std", - "time", ] } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] From 2ce1e261732c898e9d2c26e9fec861dc83300e0c Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 11:19:22 +0200 Subject: [PATCH 06/12] Restructure feature flags --- netwatch/Cargo.toml | 4 ++-- netwatch/build.rs | 4 +++- netwatch/src/interfaces/{fallback.rs => posix_minimal.rs} | 0 netwatch/src/{ip_fallback.rs => ip_posix_minimal.rs} | 0 netwatch/src/lib.rs | 4 ++-- netwatch/src/netmon.rs | 4 ++-- netwatch/src/netmon/actor.rs | 4 ++-- netwatch/src/netmon/{fallback.rs => posix_minimal.rs} | 8 +++++--- 8 files changed, 16 insertions(+), 12 deletions(-) rename netwatch/src/interfaces/{fallback.rs => posix_minimal.rs} (100%) rename netwatch/src/{ip_fallback.rs => ip_posix_minimal.rs} (100%) rename netwatch/src/netmon/{fallback.rs => posix_minimal.rs} (65%) diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index e1ffbef1..0ac500d0 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -41,8 +41,8 @@ noq-udp = "0.9" libc = "0.2.139" socket2 = { version = "0.6", features = ["all"] } -# netdev is only available on desktop/mobile platforms -[target.'cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios", target_os = "windows", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] +# netdev is available on all non-embedded, non-wasm platforms +[target.'cfg(not(any(target_os = "espidf", all(target_family = "wasm", target_os = "unknown"))))'.dependencies] netdev = "0.40.1" tokio = { version = "1", features = [ "fs", diff --git a/netwatch/build.rs b/netwatch/build.rs index 73227086..0df16302 100644 --- a/netwatch/build.rs +++ b/netwatch/build.rs @@ -5,7 +5,9 @@ fn main() { cfg_aliases! { // Convenience aliases 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 - has_netdev: { any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios", target_os = "windows", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd") }, + has_netdev: { not(any(posix_minimal, wasm_browser)) }, } } diff --git a/netwatch/src/interfaces/fallback.rs b/netwatch/src/interfaces/posix_minimal.rs similarity index 100% rename from netwatch/src/interfaces/fallback.rs rename to netwatch/src/interfaces/posix_minimal.rs diff --git a/netwatch/src/ip_fallback.rs b/netwatch/src/ip_posix_minimal.rs similarity index 100% rename from netwatch/src/ip_fallback.rs rename to netwatch/src/ip_posix_minimal.rs diff --git a/netwatch/src/lib.rs b/netwatch/src/lib.rs index 4572390c..cb93968d 100644 --- a/netwatch/src/lib.rs +++ b/netwatch/src/lib.rs @@ -1,9 +1,9 @@ //! Networking related utilities #[cfg_attr(wasm_browser, path = "interfaces/wasm_browser.rs")] -#[cfg_attr(not(any(has_netdev, wasm_browser)), path = "interfaces/fallback.rs")] +#[cfg_attr(posix_minimal, path = "interfaces/posix_minimal.rs")] pub mod interfaces; -#[cfg_attr(not(any(has_netdev, wasm_browser)), path = "ip_fallback.rs")] +#[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 a518b480..0b35fbbf 100644 --- a/netwatch/src/netmon.rs +++ b/netwatch/src/netmon.rs @@ -18,12 +18,12 @@ mod android; mod bsd; #[cfg(target_os = "linux")] mod linux; +#[cfg(posix_minimal)] +mod posix_minimal; #[cfg(wasm_browser)] mod wasm_browser; #[cfg(target_os = "windows")] mod windows; -#[cfg(not(any(has_netdev, wasm_browser)))] -mod fallback; #[cfg(not(wasm_browser))] pub(crate) use self::actor::is_interesting_interface; diff --git a/netwatch/src/netmon/actor.rs b/netwatch/src/netmon/actor.rs index ac56e93e..5eba6038 100644 --- a/netwatch/src/netmon/actor.rs +++ b/netwatch/src/netmon/actor.rs @@ -19,12 +19,12 @@ use super::android as os; use super::bsd as os; #[cfg(target_os = "linux")] use super::linux as os; +#[cfg(posix_minimal)] +use super::posix_minimal as os; #[cfg(wasm_browser)] use super::wasm_browser as os; #[cfg(target_os = "windows")] use super::windows as os; -#[cfg(not(any(has_netdev, wasm_browser)))] -use super::fallback as os; use crate::interfaces::State; /// The message sent by the OS specific monitors. diff --git a/netwatch/src/netmon/fallback.rs b/netwatch/src/netmon/posix_minimal.rs similarity index 65% rename from netwatch/src/netmon/fallback.rs rename to netwatch/src/netmon/posix_minimal.rs index d2bbcb92..ec11ca9a 100644 --- a/netwatch/src/netmon/fallback.rs +++ b/netwatch/src/netmon/posix_minimal.rs @@ -10,11 +10,13 @@ use super::actor::NetworkMessage; pub struct Error; #[derive(Debug)] -pub(super) struct RouteMonitor; +pub(super) struct RouteMonitor { + _sender: mpsc::Sender, +} impl RouteMonitor { - pub(super) fn new(_sender: mpsc::Sender) -> Result { - Ok(RouteMonitor) + pub(super) fn new(sender: mpsc::Sender) -> Result { + Ok(RouteMonitor { _sender: sender }) } } From 44cc5c3050f1f1525729c749496d7025432c360e Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 11:33:40 +0200 Subject: [PATCH 07/12] Include portmapper stubs. --- .github/workflows/ci.yaml | 4 ++-- Cargo.lock | 14 ++++++++++++-- netwatch/src/interfaces/posix_minimal.rs | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6714dd97..24ef3f0c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -158,8 +158,8 @@ jobs: with: components: rust-src - uses: Swatinem/rust-cache@v2 - - name: Check netwatch for ESP32-C3 - run: cargo check -Z build-std=std,panic_abort --target riscv32imc-esp-espidf -p netwatch --no-default-features + - name: Check workspace for ESP32-C3 + run: cargo check -Z build-std=std,panic_abort --target riscv32imc-esp-espidf --workspace --no-default-features wasm_test: name: Build & test wasm32 for browsers diff --git a/Cargo.lock b/Cargo.lock index c842916f..a2a61071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -751,13 +751,14 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iroh-metrics" -version = "0.38.2" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c946095f060e6e59b9ff30cc26c75cdb758e7fb0cde8312c89e2144654989fcb" +checksum = "761b45ba046134b11eb3e432fa501616b45c4bf3a30c21717578bc07aa6461dd" dependencies = [ "iroh-metrics-derive", "itoa", "n0-error", + "portable-atomic", "postcard", "ryu", "serde", @@ -1269,6 +1270,15 @@ dependencies = [ "time", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "serde", +] + [[package]] name = "portmapper" version = "0.14.0" diff --git a/netwatch/src/interfaces/posix_minimal.rs b/netwatch/src/interfaces/posix_minimal.rs index d3de3164..7b3e7166 100644 --- a/netwatch/src/interfaces/posix_minimal.rs +++ b/netwatch/src/interfaces/posix_minimal.rs @@ -73,6 +73,22 @@ impl IpNet { } } +/// 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 + } +} + /// Intended to store the state of the machine's network interfaces. #[derive(Debug, PartialEq, Eq, Clone)] pub struct State { From 1fcc8abeb833e6ab21b319973333ba63326d8b0e Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 11:41:56 +0200 Subject: [PATCH 08/12] Rework cfg flags and fix unused imports in esp32 --- netwatch/build.rs | 2 -- netwatch/src/ip_posix_minimal.rs | 11 ----------- netwatch/src/netmon.rs | 2 +- netwatch/src/netmon/actor.rs | 2 +- netwatch/src/netmon/posix_minimal.rs | 4 ---- 5 files changed, 2 insertions(+), 19 deletions(-) diff --git a/netwatch/build.rs b/netwatch/build.rs index 0df16302..e4f9a694 100644 --- a/netwatch/build.rs +++ b/netwatch/build.rs @@ -7,7 +7,5 @@ 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 - has_netdev: { not(any(posix_minimal, wasm_browser)) }, } } diff --git a/netwatch/src/ip_posix_minimal.rs b/netwatch/src/ip_posix_minimal.rs index e640ef58..38c59848 100644 --- a/netwatch/src/ip_posix_minimal.rs +++ b/netwatch/src/ip_posix_minimal.rs @@ -1,7 +1,5 @@ //! IP address related utilities — fallback for platforms without `netdev`. -use std::net::Ipv6Addr; - /// List of machine's IP addresses (stub). #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct LocalAddresses { @@ -10,12 +8,3 @@ pub struct LocalAddresses { /// Regular addresses. pub regular: Vec, } - -pub(crate) fn is_private_v6(ip: &Ipv6Addr) -> bool { - ip.octets()[0] & 0xfe == 0xfc -} - -/// Returns true if the address is a unicast address with link-local scope, as defined in RFC 4291. -pub const fn is_unicast_link_local(addr: Ipv6Addr) -> bool { - (addr.segments()[0] & 0xffc0) == 0xfe80 -} diff --git a/netwatch/src/netmon.rs b/netwatch/src/netmon.rs index 0b35fbbf..c7c9ffc7 100644 --- a/netwatch/src/netmon.rs +++ b/netwatch/src/netmon.rs @@ -25,7 +25,7 @@ mod wasm_browser; #[cfg(target_os = "windows")] mod windows; -#[cfg(not(wasm_browser))] +#[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 5eba6038..a2783df2 100644 --- a/netwatch/src/netmon/actor.rs +++ b/netwatch/src/netmon/actor.rs @@ -2,7 +2,7 @@ use n0_future::time::{self, Duration, Instant}; use n0_watcher::Watchable; pub(super) use os::Error; use os::RouteMonitor; -#[cfg(not(wasm_browser))] +#[cfg(not(any(posix_minimal, wasm_browser)))] pub(crate) use os::is_interesting_interface; use tokio::sync::mpsc; use tracing::{debug, trace}; diff --git a/netwatch/src/netmon/posix_minimal.rs b/netwatch/src/netmon/posix_minimal.rs index ec11ca9a..5ede6601 100644 --- a/netwatch/src/netmon/posix_minimal.rs +++ b/netwatch/src/netmon/posix_minimal.rs @@ -19,7 +19,3 @@ impl RouteMonitor { Ok(RouteMonitor { _sender: sender }) } } - -pub(crate) fn is_interesting_interface(_name: &str) -> bool { - true -} From ae25e21733283e76f12ed3cfe9272e46bfaf4c4c Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 11:45:49 +0200 Subject: [PATCH 09/12] Use sccache and fix deps for wasm --- .github/workflows/ci.yaml | 5 ++++- netwatch/Cargo.toml | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 24ef3f0c..bcefd2cf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -152,12 +152,15 @@ jobs: if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')" timeout-minutes: 30 runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "on" steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@nightly with: components: rust-src - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.9 - name: Check workspace for ESP32-C3 run: cargo check -Z build-std=std,panic_abort --target riscv32imc-esp-espidf --workspace --no-default-features diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index 0ac500d0..cebb6794 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -28,8 +28,6 @@ tokio = { version = "1", features = [ "io-util", "macros", "sync", - "rt", - "net", "time", ] } tokio-util = { version = "0.7", features = ["rt"] } @@ -40,6 +38,7 @@ tracing = "0.1" noq-udp = "0.9" libc = "0.2.139" socket2 = { version = "0.6", features = ["all"] } +tokio = { version = "1", features = ["rt", "net"] } # netdev is available on all non-embedded, non-wasm platforms [target.'cfg(not(any(target_os = "espidf", all(target_family = "wasm", target_os = "unknown"))))'.dependencies] From fc3795682662d8fd617cdcaa884be7f1c33f7701 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 12:01:54 +0200 Subject: [PATCH 10/12] move ipnet to non wasm section. --- netwatch/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index cebb6794..1414c12c 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -18,7 +18,6 @@ workspace = true [dependencies] atomic-waker = "1.1.2" bytes = "1.7" -ipnet = "2" n0-error = "0.1.2" n0-future = "0.3.1" n0-watcher = "0.6.0" @@ -35,6 +34,7 @@ tracing = "0.1" # non-browser dependencies [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] +ipnet = "2" noq-udp = "0.9" libc = "0.2.139" socket2 = { version = "0.6", features = ["all"] } From 82e9747c3b9e87da27945621c22c45ff9468319b Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 12:18:35 +0200 Subject: [PATCH 11/12] Add poll based posix minimal implementation and test in ci. --- .github/workflows/ci.yaml | 16 ++ netwatch/src/interfaces/posix_minimal.rs | 258 +++++++++++++++++++++-- netwatch/src/ip_posix_minimal.rs | 39 +++- netwatch/src/netmon.rs | 21 +- netwatch/src/netmon/actor.rs | 21 +- netwatch/src/netmon/posix_minimal.rs | 25 ++- 6 files changed, 338 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bcefd2cf..016e62d7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -147,6 +147,22 @@ jobs: env: RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG' }} + test_posix_minimal: + name: Test posix_minimal (netwatch) + if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')" + timeout-minutes: 30 + runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "on" + RUSTFLAGS: "-Dwarnings --cfg posix_minimal" + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: mozilla-actions/sccache-action@v0.0.9 + - name: Build and test netwatch (posix_minimal) + run: cargo test --locked -p netwatch + esp32_check: name: ESP32-C3 build check if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')" diff --git a/netwatch/src/interfaces/posix_minimal.rs b/netwatch/src/interfaces/posix_minimal.rs index 7b3e7166..33db3eb2 100644 --- a/netwatch/src/interfaces/posix_minimal.rs +++ b/netwatch/src/interfaces/posix_minimal.rs @@ -1,37 +1,165 @@ -//! Fallback interfaces implementation for platforms without `netdev`. -//! Provides stub types — no network interface enumeration available. +//! Minimal POSIX interfaces implementation using `getifaddrs`. +//! +//! Used on platforms (like ESP-IDF) where `netdev` is not available but +//! standard POSIX networking APIs are supported. -use std::{collections::HashMap, fmt, net::IpAddr}; +use std::{ + collections::HashMap, + ffi::CStr, + fmt, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; pub(crate) use ipnet::{Ipv4Net, Ipv6Net}; use n0_future::time::Instant; use crate::ip::LocalAddresses; -/// Represents a network interface (stub). +// POSIX interface flags — standard values across all POSIX systems. +const IFF_UP: u32 = 0x1; +const IFF_LOOPBACK: u32 = 0x8; + +/// FFI declarations for `getifaddrs`/`freeifaddrs`. +/// +/// We declare these manually because the `libc` crate may not expose them +/// for all targets (e.g. espidf). ESP-IDF's lwIP provides these since IDF v5.0. +mod ffi { + #[repr(C)] + pub(super) struct ifaddrs { + pub ifa_next: *mut ifaddrs, + pub ifa_name: *mut libc::c_char, + pub ifa_flags: libc::c_uint, + pub ifa_addr: *mut libc::sockaddr, + pub ifa_netmask: *mut libc::sockaddr, + pub ifa_ifu: *mut libc::sockaddr, + pub ifa_data: *mut libc::c_void, + } + + unsafe extern "C" { + pub(super) fn getifaddrs(ifap: *mut *mut ifaddrs) -> libc::c_int; + pub(super) fn freeifaddrs(ifa: *mut ifaddrs); + } +} + +/// Extract an IP address from a raw `sockaddr` pointer. +/// +/// # Safety +/// The pointer must be null or point to a valid `sockaddr_in` or `sockaddr_in6`. +unsafe fn sockaddr_to_ip(sa: *const libc::sockaddr) -> Option { + if sa.is_null() { + return None; + } + // Safety: caller guarantees sa is valid + unsafe { + match (*sa).sa_family as i32 { + libc::AF_INET => { + let sa_in = sa as *const libc::sockaddr_in; + let ip = Ipv4Addr::from(u32::from_be((*sa_in).sin_addr.s_addr)); + Some(IpAddr::V4(ip)) + } + libc::AF_INET6 => { + let sa_in6 = sa as *const libc::sockaddr_in6; + let ip = Ipv6Addr::from((*sa_in6).sin6_addr.s6_addr); + Some(IpAddr::V6(ip)) + } + _ => None, + } + } +} + +/// Convert a netmask IP to a prefix length by counting leading ones. +fn prefix_len(mask: IpAddr) -> u8 { + match mask { + IpAddr::V4(m) => u32::from_be_bytes(m.octets()).leading_ones() as u8, + IpAddr::V6(m) => u128::from_be_bytes(m.octets()).leading_ones() as u8, + } +} + +/// A single address entry from `getifaddrs`. +struct IfAddrEntry { + name: String, + flags: u32, + addr: Option, + netmask: Option, +} + +/// Call `getifaddrs` and collect all entries. +fn enumerate_ifaddrs() -> Vec { + let mut result = Vec::new(); + let mut ifap: *mut ffi::ifaddrs = std::ptr::null_mut(); + + // Safety: getifaddrs is a standard POSIX function. + unsafe { + if ffi::getifaddrs(&mut ifap) != 0 { + return result; + } + + let mut cursor = ifap; + while !cursor.is_null() { + let ifa = &*cursor; + if !ifa.ifa_name.is_null() { + let name = CStr::from_ptr(ifa.ifa_name).to_string_lossy().into_owned(); + let flags = ifa.ifa_flags; + let addr = sockaddr_to_ip(ifa.ifa_addr); + let netmask = sockaddr_to_ip(ifa.ifa_netmask); + result.push(IfAddrEntry { + name, + flags, + addr, + netmask, + }); + } + cursor = ifa.ifa_next; + } + + ffi::freeifaddrs(ifap); + } + + result +} + +/// Represents a network interface. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Interface; +pub struct Interface { + name: String, + flags: u32, + ipv4: Vec, + ipv6: Vec, +} impl fmt::Display for Interface { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "unknown") + write!( + f, + "{} ipv4={:?} ipv6={:?}", + self.name, self.ipv4, self.ipv6 + ) } } impl Interface { /// Is this interface up? pub fn is_up(&self) -> bool { - false + self.flags & IFF_UP != 0 } /// The name of the interface. pub fn name(&self) -> &str { - "unknown" + &self.name + } + + /// Is this a loopback interface? + pub(crate) fn is_loopback(&self) -> bool { + self.flags & IFF_LOOPBACK != 0 } /// A list of all ip addresses of this interface. pub fn addrs(&self) -> impl Iterator + '_ { - std::iter::empty() + self.ipv4 + .iter() + .cloned() + .map(IpNet::V4) + .chain(self.ipv6.iter().cloned().map(IpNet::V6)) } } @@ -73,7 +201,9 @@ impl IpNet { } } -/// The router/gateway of the local network (stub — always returns `None`). +/// The router/gateway of the local network. +/// +/// Gateway discovery is not available on this platform. #[derive(Debug, Clone)] pub struct HomeRouter { /// Ip of the router. @@ -89,6 +219,48 @@ impl HomeRouter { } } +/// Collect `getifaddrs` entries into a map of `Interface` structs grouped by name. +fn collect_interfaces() -> HashMap { + let entries = enumerate_ifaddrs(); + let mut map: HashMap = HashMap::new(); + + for entry in entries { + let iface = map.entry(entry.name.clone()).or_insert_with(|| Interface { + name: entry.name, + flags: entry.flags, + ipv4: Vec::new(), + ipv6: Vec::new(), + }); + + // Merge flags (take the union). + iface.flags |= entry.flags; + + if let Some(addr) = entry.addr { + let pfx = entry.netmask.map(prefix_len); + match addr { + IpAddr::V4(v4) => { + if let Ok(net) = Ipv4Net::new(v4, pfx.unwrap_or(32)) { + iface.ipv4.push(net); + } + } + IpAddr::V6(v6) => { + if let Ok(net) = Ipv6Net::new(v6, pfx.unwrap_or(128)) { + iface.ipv6.push(net); + } + } + } + } + } + + // Sort addresses for stable comparison. + for iface in map.values_mut() { + iface.ipv4.sort(); + iface.ipv6.sort(); + } + + map +} + /// Intended to store the state of the machine's network interfaces. #[derive(Debug, PartialEq, Eq, Clone)] pub struct State { @@ -110,18 +282,72 @@ pub struct State { impl fmt::Display for State { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "fallback(no interfaces)") + for iface in self.interfaces.values() { + write!(f, "{iface}")?; + if let Some(ref default_if) = self.default_route_interface + && iface.name() == default_if + { + write!(f, " (default)")?; + } + if f.alternate() { + writeln!(f)?; + } else { + write!(f, "; ")?; + } + } + Ok(()) + } +} + +/// Reports whether ip is a usable IPv4 address. +fn is_usable_v4(ip: &IpAddr) -> bool { + ip.is_ipv4() && !ip.is_loopback() +} + +/// Reports whether ip is a usable IPv6 address (global or ULA). +fn is_usable_v6(ip: &IpAddr) -> bool { + match ip { + IpAddr::V6(ip) => { + let segment1 = ip.segments()[0]; + // Global unicast: 2000::/3 + if (segment1 & 0xe000) == 0x2000 { + return true; + } + // Unique local: fc00::/7 + ip.octets()[0] & 0xfe == 0xfc + } + IpAddr::V4(_) => false, } } impl State { - /// Returns a default empty state (no interface enumeration on this platform). + /// Returns the state of all the current machine's network interfaces. pub async fn new() -> Self { + let interfaces = collect_interfaces(); + let local_addresses = LocalAddresses::from_interfaces(interfaces.values()); + + let mut have_v4 = false; + let mut have_v6 = false; + + for iface in interfaces.values() { + if !iface.is_up() { + continue; + } + for pfx in iface.addrs() { + let addr = pfx.addr(); + if addr.is_loopback() { + continue; + } + have_v4 |= is_usable_v4(&addr); + have_v6 |= is_usable_v6(&addr); + } + } + State { - interfaces: HashMap::new(), - local_addresses: LocalAddresses::default(), - have_v6: false, - have_v4: true, + interfaces, + local_addresses, + have_v4, + have_v6, is_expensive: false, default_route_interface: None, last_unsuspend: None, diff --git a/netwatch/src/ip_posix_minimal.rs b/netwatch/src/ip_posix_minimal.rs index 38c59848..398d71e3 100644 --- a/netwatch/src/ip_posix_minimal.rs +++ b/netwatch/src/ip_posix_minimal.rs @@ -1,10 +1,41 @@ -//! IP address related utilities — fallback for platforms without `netdev`. +//! IP address related utilities — minimal POSIX implementation. -/// List of machine's IP addresses (stub). +use std::net::IpAddr; + +use crate::interfaces::Interface; + +/// List of machine's IP addresses. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct LocalAddresses { /// Loopback addresses. - pub loopback: Vec, + pub loopback: Vec, /// Regular addresses. - pub regular: Vec, + pub regular: Vec, +} + +impl LocalAddresses { + /// Build local addresses from already-enumerated interfaces. + pub(crate) fn from_interfaces<'a>(ifaces: impl Iterator) -> Self { + let mut loopback = Vec::new(); + let mut regular = Vec::new(); + + for iface in ifaces { + if !iface.is_up() { + continue; + } + for pfx in iface.addrs() { + let ip = pfx.addr(); + if ip.is_loopback() || iface.is_loopback() { + loopback.push(ip); + } else { + regular.push(ip); + } + } + } + + loopback.sort(); + regular.sort(); + + LocalAddresses { loopback, regular } + } } diff --git a/netwatch/src/netmon.rs b/netwatch/src/netmon.rs index c7c9ffc7..426fb569 100644 --- a/netwatch/src/netmon.rs +++ b/netwatch/src/netmon.rs @@ -6,23 +6,26 @@ use n0_watcher::Watchable; use tokio::sync::{mpsc, oneshot}; mod actor; -#[cfg(target_os = "android")] +#[cfg(all(target_os = "android", not(posix_minimal)))] mod android; -#[cfg(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "macos", - target_os = "ios" +#[cfg(all( + any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + target_os = "ios" + ), + not(posix_minimal) ))] mod bsd; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(posix_minimal)))] mod linux; #[cfg(posix_minimal)] mod posix_minimal; #[cfg(wasm_browser)] mod wasm_browser; -#[cfg(target_os = "windows")] +#[cfg(all(target_os = "windows", not(posix_minimal)))] mod windows; #[cfg(not(any(posix_minimal, wasm_browser)))] diff --git a/netwatch/src/netmon/actor.rs b/netwatch/src/netmon/actor.rs index a2783df2..ad337168 100644 --- a/netwatch/src/netmon/actor.rs +++ b/netwatch/src/netmon/actor.rs @@ -7,23 +7,26 @@ pub(crate) use os::is_interesting_interface; use tokio::sync::mpsc; use tracing::{debug, trace}; -#[cfg(target_os = "android")] +#[cfg(all(target_os = "android", not(posix_minimal)))] use super::android as os; -#[cfg(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "macos", - target_os = "ios" +#[cfg(all( + any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + target_os = "ios" + ), + not(posix_minimal) ))] use super::bsd as os; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(posix_minimal)))] use super::linux as os; #[cfg(posix_minimal)] use super::posix_minimal as os; #[cfg(wasm_browser)] use super::wasm_browser as os; -#[cfg(target_os = "windows")] +#[cfg(all(target_os = "windows", not(posix_minimal)))] use super::windows as os; use crate::interfaces::State; diff --git a/netwatch/src/netmon/posix_minimal.rs b/netwatch/src/netmon/posix_minimal.rs index 5ede6601..a84fb523 100644 --- a/netwatch/src/netmon/posix_minimal.rs +++ b/netwatch/src/netmon/posix_minimal.rs @@ -1,21 +1,38 @@ -//! Fallback netmon implementation for platforms without OS-specific route monitoring. -//! Does nothing — no route monitoring available. +//! Polling-based route monitor for platforms without OS-specific change notifications. +//! +//! Since ESP-IDF doesn't have netlink or routing sockets, we poll for changes +//! on a fixed interval and let the actor debounce and diff. use n0_error::stack_error; +use n0_future::time::Duration; use tokio::sync::mpsc; use super::actor::NetworkMessage; +/// Poll interval for checking network changes. +const POLL_INTERVAL: Duration = Duration::from_secs(30); + #[stack_error(derive, add_meta)] pub struct Error; #[derive(Debug)] pub(super) struct RouteMonitor { - _sender: mpsc::Sender, + _handle: tokio::task::JoinHandle<()>, } impl RouteMonitor { pub(super) fn new(sender: mpsc::Sender) -> Result { - Ok(RouteMonitor { _sender: sender }) + let handle = tokio::spawn(async move { + let mut interval = tokio::time::interval(POLL_INTERVAL); + // Skip the immediate first tick. + interval.tick().await; + loop { + interval.tick().await; + if sender.send(NetworkMessage::Change).await.is_err() { + break; + } + } + }); + Ok(RouteMonitor { _handle: handle }) } } From e632e746f4c23bda06e2c4b5568c6bbaab0c93fb Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 11 Mar 2026 12:33:48 +0200 Subject: [PATCH 12/12] fmt --- netwatch/src/interfaces/posix_minimal.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/netwatch/src/interfaces/posix_minimal.rs b/netwatch/src/interfaces/posix_minimal.rs index 33db3eb2..aff5727e 100644 --- a/netwatch/src/interfaces/posix_minimal.rs +++ b/netwatch/src/interfaces/posix_minimal.rs @@ -129,11 +129,7 @@ pub struct Interface { impl fmt::Display for Interface { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} ipv4={:?} ipv6={:?}", - self.name, self.ipv4, self.ipv6 - ) + write!(f, "{} ipv4={:?} ipv6={:?}", self.name, self.ipv4, self.ipv6) } }