Skip to content
Merged
363 changes: 357 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions netwatch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,24 @@ workspace = true
[dependencies]
atomic-waker = "1.1.2"
bytes = "1.7"
futures-lite = "2.5"
futures-sink = "0.3"
futures-util = "0.3"
n0-future = "0.1.1"
thiserror = "2"
time = "0.3.20"
tokio = { version = "1", features = [
"io-util",
"macros",
"sync",
"time",
] }
tokio-util = { version = "0.7", features = ["rt"] }
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.5.5" }
libc = "0.2.139"
netdev = "0.31.0"
once_cell = "1.18.0"
quinn-udp = { package = "iroh-quinn-udp", version = "0.5.5" }
socket2 = "0.5.3"
thiserror = "2"
time = "0.3.20"
tokio = { version = "1", features = [
"io-util",
"macros",
Expand All @@ -40,8 +48,6 @@ tokio = { version = "1", features = [
"process",
"time",
] }
tokio-util = { version = "0.7", features = ["rt"] }
tracing = "0.1"

[target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dependencies]
netlink-packet-core = "0.7.0"
Expand All @@ -62,6 +68,10 @@ windows-result = "0.3"
serde = { version = "1", features = ["derive"] }
derive_more = { version = "1.0.0", features = ["debug"] }

[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
js-sys = "0.3"
web-sys = { version = "0.3", features = ["EventListener", "EventTarget"] }

[dev-dependencies]
testresult = "0.4.1"
tokio = { version = "1", features = [
Expand Down
4 changes: 2 additions & 2 deletions netwatch/src/interfaces/bsd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::LazyLock,
};

use libc::{c_int, uintptr_t, AF_INET, AF_INET6, AF_LINK, AF_ROUTE, AF_UNSPEC, CTL_NET};
#[cfg(any(target_os = "macos", target_os = "ios"))]
use libc::{
NET_RT_DUMP, RTAX_BRD, RTAX_DST, RTAX_GATEWAY, RTAX_MAX, RTAX_NETMASK, RTA_IFP, RTF_GATEWAY,
};
use once_cell::sync::Lazy;
use tracing::warn;

use super::DefaultRouteDetails;
Expand Down Expand Up @@ -459,7 +459,7 @@ enum MessageType {
InterfaceAnnounce,
}

static ROUTING_STACK: Lazy<RoutingStack> = Lazy::new(probe_routing_stack);
static ROUTING_STACK: LazyLock<RoutingStack> = LazyLock::new(probe_routing_stack);

struct RoutingStack {
rtm_version: i32,
Expand Down
2 changes: 1 addition & 1 deletion netwatch/src/interfaces/linux.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Linux-specific network interfaces implementations.

#[cfg(not(target_os = "android"))]
use futures_util::TryStreamExt;
use n0_future::TryStreamExt;
use tokio::{
fs::File,
io::{AsyncBufReadExt, BufReader},
Expand Down
118 changes: 118 additions & 0 deletions netwatch/src/interfaces/wasm_browser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::{collections::HashMap, fmt};

use js_sys::{JsString, Reflect};

pub const BROWSER_INTERFACE: &str = "browserif";

/// Represents a network interface.
#[derive(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)
}
}

impl Interface {
async fn new() -> Self {
let is_up = Self::is_up();
tracing::debug!(onLine = is_up, "Fetched globalThis.navigator.onLine");
Self {
is_up: is_up.unwrap_or(true),
}
}

fn is_up() -> Option<bool> {
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()
}

/// The name of the interface.
pub(crate) fn name(&self) -> &str {
BROWSER_INTERFACE
}
}

/// 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)]
pub struct State {
/// Maps from an interface name interface.
pub interfaces: HashMap<String, Interface>,

/// 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<String>,

/// The HTTP proxy to use, if any.
pub(crate) http_proxy: Option<String>,

/// The URL to the Proxy Autoconfig URL, if applicable.
pub(crate) pac: Option<String>,
}

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(())
}
}

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,
}
}
}
14 changes: 13 additions & 1 deletion netwatch/src/ip.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
//! IP address related utilities.

use std::net::{IpAddr, Ipv6Addr};
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
use std::net::IpAddr;
use std::net::Ipv6Addr;

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
const IFF_UP: u32 = 0x1;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
const IFF_LOOPBACK: u32 = 0x8;

/// List of machine's IP addresses.
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalAddresses {
/// Loopback addresses.
Expand All @@ -14,12 +19,14 @@ pub struct LocalAddresses {
pub regular: Vec<IpAddr>,
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
impl Default for LocalAddresses {
fn default() -> Self {
Self::new()
}
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
impl LocalAddresses {
/// Returns the machine's IP addresses.
/// If there are no regular addresses it will return any IPv4 linklocal or IPv6 unique local
Expand Down Expand Up @@ -91,17 +98,20 @@ impl LocalAddresses {
}
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
pub(crate) const fn is_up(interface: &netdev::Interface) -> bool {
interface.flags & IFF_UP != 0
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
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
/// (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(all(target_family = "wasm", target_os = "unknown")))]
pub(crate) fn is_private(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(ip) => {
Expand All @@ -116,11 +126,13 @@ pub(crate) fn is_private(ip: &IpAddr) -> bool {
}
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
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(all(target_family = "wasm", target_os = "unknown")))]
pub(super) fn is_link_local(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(ip) => ip.is_link_local(),
Expand Down
1 change: 1 addition & 0 deletions netwatch/src/ip_family.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ impl IpFamily {
}
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
impl From<IpFamily> for socket2::Domain {
fn from(value: IpFamily) -> Self {
match value {
Expand Down
9 changes: 8 additions & 1 deletion netwatch/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
//! Networking related utilities

#[cfg_attr(
all(target_family = "wasm", target_os = "unknown"),
path = "interfaces/wasm_browser.rs"
)]
pub mod interfaces;
pub mod ip;
mod ip_family;
pub mod netmon;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
mod udp;

pub use self::{ip_family::IpFamily, udp::UdpSocket};
pub use self::ip_family::IpFamily;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
pub use self::udp::UdpSocket;
12 changes: 8 additions & 4 deletions netwatch/src/netmon.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! Monitoring of networking interfaces and route changes.

use futures_lite::future::Boxed as BoxFuture;
use n0_future::{
boxed::BoxFuture,
task::{self, AbortOnDropHandle},
};
use tokio::sync::{mpsc, oneshot};
use tokio_util::task::AbortOnDropHandle;

mod actor;
#[cfg(target_os = "android")]
Expand All @@ -17,6 +19,8 @@ mod android;
mod bsd;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
mod wasm_browser;
#[cfg(target_os = "windows")]
mod windows;

Expand Down Expand Up @@ -57,7 +61,7 @@ impl Monitor {
let actor = Actor::new().await?;
let actor_tx = actor.subscribe();

let handle = tokio::task::spawn(async move {
let handle = task::spawn(async move {
actor.run().await;
});

Expand Down Expand Up @@ -99,7 +103,7 @@ impl Monitor {

#[cfg(test)]
mod tests {
use futures_util::FutureExt;
use n0_future::future::FutureExt;

use super::*;

Expand Down
Loading
Loading