Skip to content

Commit

Permalink
feat(iroh-net): implement network monitoring (#1472)
Browse files Browse the repository at this point in the history
## Description

Implements #1464

## Outstanding work

- [x] Integration of monitoring
- [x] OS Specific monitoring
  - [x] macos
  - [x] windows
  - [x] linux
  - [x] android (unusable dummy)
- [x] sleep detection (wall time jumps)
- [x] testing: only manual for now, and a smoke test, hard to test
otherwise
- [x] ~~metrics~~: I don't think we need those for now
- [x] cleanup & shutdown

## Notes

Android sucks, seriously google what are you doing. It is not possible
to use rtnetlink anymore on android, and the `ConnectivityManager` can
only be used from Java/Kotlin. So for now we don't do anything on
android.
The solution is to inject events from the `ConnectivityManager` through
a public API, like tailscale does.. 😭 😭 😠


## Change checklist

- [x] Self-review.
- [x] Documentation updates if relevant.
- [x] Tests if relevant.

---------

Co-authored-by: Divma <26765164+divagant-martian@users.noreply.github.com>
  • Loading branch information
dignifiedquire and divagant-martian authored Sep 15, 2023
1 parent 36e622f commit a89078f
Show file tree
Hide file tree
Showing 13 changed files with 1,067 additions and 102 deletions.
28 changes: 25 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions iroh-net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,14 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = tr
iroh-metrics = { version = "0.6.0-alpha.1", path = "../iroh-metrics", default-features = false }

[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
netlink-packet-core = "0.7.0"
netlink-packet-route = "0.17.0"
netlink-sys = "0.8.5"
rtnetlink = "0.13.0"

[target.'cfg(target_os = "windows")'.dependencies]
wmi = "0.13"
windows = { version = "0.51", features = ["Win32_NetworkManagement_IpHelper", "Win32_Foundation", "Win32_NetworkManagement_Ndis", "Win32_Networking_WinSock"] }

[dev-dependencies]
clap = { version = "4", features = ["derive"] }
Expand Down
36 changes: 34 additions & 2 deletions iroh-net/src/magicsock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use std::{

use anyhow::{bail, Context as _, Result};
use bytes::Bytes;
use futures::future::BoxFuture;
use futures::{future::BoxFuture, FutureExt};
use iroh_metrics::{inc, inc_by};
use quinn::AsyncUdpSocket;
use rand::{seq::SliceRandom, Rng, SeedableRng};
Expand All @@ -47,9 +47,10 @@ use crate::{
config::{self, DERP_MAGIC_IP},
derp::{DerpMap, DerpRegion},
disco,
dns::DNS_RESOLVER,
key::{PublicKey, SecretKey, SharedSecret},
magic_endpoint::NodeAddr,
net::ip::LocalAddresses,
net::{ip::LocalAddresses, netmon},
netcheck, portmapper, stun,
util::AbortingJoinHandle,
};
Expand Down Expand Up @@ -917,6 +918,37 @@ struct Actor {

impl Actor {
async fn run(mut self) -> Result<()> {
// Setup network monitoring
let monitor = netmon::Monitor::new().await?;
let sender = self.msg_sender.clone();
let _token = monitor
.subscribe(move |is_major| {
let sender = sender.clone();
async move {
info!("link change detected: major? {}", is_major);

// Clear DNS cache
DNS_RESOLVER.clear_cache();

if is_major {
let (s, r) = sync::oneshot::channel();
sender.send(ActorMessage::RebindAll(s)).await.ok();
sender
.send(ActorMessage::ReStun("link-change-major"))
.await
.ok();
r.await.ok();
} else {
sender
.send(ActorMessage::ReStun("link-change-minor"))
.await
.ok();
}
}
.boxed()
})
.await?;

// Let the the hearbeat only start a couple seconds later
let mut endpoint_heartbeat_timer = time::interval_at(
time::Instant::now() + Duration::from_secs(5),
Expand Down
1 change: 1 addition & 0 deletions iroh-net/src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
pub mod interfaces;
pub mod ip;
pub mod netmon;
101 changes: 15 additions & 86 deletions iroh-net/src/net/interfaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::{collections::HashMap, net::IpAddr};
target_os = "macos",
target_os = "ios"
))]
mod bsd;
pub(super) mod bsd;
#[cfg(any(target_os = "linux", target_os = "android"))]
mod linux;
#[cfg(target_os = "windows")]
Expand Down Expand Up @@ -61,6 +61,11 @@ impl Interface {
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<Item = IpNet> + '_ {
self.iface
Expand Down Expand Up @@ -149,12 +154,10 @@ impl IpNet {

/// 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)]
#[derive(Debug, PartialEq, Eq)]
pub struct State {
/// Maps from an interface name to the IP addresses configured on that interface.
pub interface_ips: HashMap<String, Vec<IpNet>>,
/// List of available interfaces, identified by name.
pub interface: HashMap<String, Interface>,
/// 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.
Expand Down Expand Up @@ -186,8 +189,7 @@ impl State {
///
/// It does not set the returned `State.is_expensive`. The caller can populate that.
pub async fn new() -> Self {
let mut interface_ips = HashMap::new();
let mut interface = HashMap::new();
let mut interfaces = HashMap::new();
let mut have_v6 = false;
let mut have_v4 = false;

Expand All @@ -208,15 +210,13 @@ impl State {
}
}

interface.insert(name.clone(), ni);
interface_ips.insert(name, pfxs);
interfaces.insert(name, ni);
}

let default_route_interface = default_route_interface().await;

State {
interface_ips,
interface,
interfaces,
have_v4,
have_v6,
is_expensive: false,
Expand All @@ -240,17 +240,7 @@ impl State {
let fake = Interface::fake();
let ifname = fake.iface.name.clone();
Self {
interface_ips: [(
ifname.clone(),
fake.iface
.ipv4
.iter()
.map(|net| IpNet::V4(net.clone()))
.collect(),
)]
.into_iter()
.collect(),
interface: [(ifname.clone(), fake)].into_iter().collect(),
interfaces: [(ifname.clone(), fake)].into_iter().collect(),
have_v6: false,
have_v4: true,
is_expensive: false,
Expand All @@ -265,50 +255,10 @@ impl State {
self.pac.is_some()
}

/// Reports whether this state and `s2` are equal, considering only interfaces in `self`
/// for which `use_interface` returns `true`, and considering only IPs for those interfaces
/// for which `use_ip` returns `true`.
pub fn equal_filtered<F, G>(&self, s2: &Self, use_interface: F, use_ip: G) -> bool
where
F: Fn(&Interface, &[IpNet]) -> bool,
G: Fn(IpAddr) -> bool,
{
if self.have_v6 != s2.have_v6
|| self.have_v4 != s2.have_v4
|| self.is_expensive != s2.is_expensive
|| self.default_route_interface != s2.default_route_interface
|| self.http_proxy != s2.http_proxy
|| self.pac != s2.pac
{
return false;
}
for (iname, i) in &self.interface {
if let Some(ips) = self.interface_ips.get(iname) {
if !use_interface(i, ips) {
continue;
}
let i2 = s2.interface.get(iname);
if i2.is_none() {
return false;
}
let i2 = i2.unwrap();
let ips2 = s2.interface_ips.get(iname);
if ips2.is_some() {
return false;
}
let ips2 = ips2.unwrap();
if i != i2 || !prefixes_equal_filtered(ips, ips2, &use_ip) {
return false;
}
}
}
true
}

/// Reports whether any interface has the provided IP address.
pub fn has_ip(&self, ip: &IpAddr) -> bool {
for pv in self.interface_ips.values() {
for p in pv {
for pv in self.interfaces.values() {
for p in pv.addrs() {
match (p, ip) {
(IpNet::V4(a), IpAddr::V4(b)) => {
if &a.addr == b {
Expand All @@ -333,27 +283,6 @@ impl State {
}
}

fn prefixes_equal_filtered<F>(a: &[IpNet], b: &[IpNet], use_ip: F) -> bool
where
F: Fn(IpAddr) -> bool,
{
if a.len() != b.len() {
return false;
}
for (a, b) in a.iter().zip(b.iter()) {
let use_a = use_ip(a.addr());
let use_b = use_ip(b.addr());
if use_a != use_b {
return false;
}
if use_a && a.addr() != b.addr() {
return false;
}
}

true
}

/// Reports whether ip is a usable IPv4 address which could
/// conceivably be used to get Internet connectivity. Globally routable and
/// private IPv4 addresses are always Usable, and link local 169.254.x.x
Expand Down
Loading

0 comments on commit a89078f

Please sign in to comment.