336 lines
12 KiB
Rust
336 lines
12 KiB
Rust
//! Derived defaults and local exclusion of overlapping overlay networks.
|
|||
|
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||
|
|
|
||
|
|
use tsunagi::Agent;
|
||
|
|
use tsunagi::discovery::SharedMemoryDiscovery;
|
||
|
|
use tsunagi::state::{DEFAULT_IPV4_RANGE, Ipv4Range, derived_ipv4_range};
|
||
|
|
use tsunagi::testing::{TestAgent, config_with, network, wait_until};
|
||
|
|
|
||
|
|
// These tests need discovery to happen before a collision, so keep the initial
|
||
|
|
// default outside the established ranges. Derived collisions have their own test.
|
||
|
|
fn network_outside(
|
||
|
|
name: &str,
|
||
|
|
ranges: &[Ipv4Range],
|
||
|
|
) -> (
|
||
|
|
tsunagi::identity::NetworkName,
|
||
|
|
tsunagi::identity::NetworkSecret,
|
||
|
|
) {
|
||
|
|
use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret};
|
||
|
|
let name = NetworkName::new(name).unwrap();
|
||
|
|
let secret = (0..=255u8)
|
||
|
|
.map(|seed| NetworkSecret::from_bytes([seed; 32]).unwrap())
|
||
|
|
.find(|secret| {
|
||
|
|
let derived = derived_ipv4_range(NetworkKeys::derive(&name, secret).network_id());
|
||
|
|
ranges.iter().all(|range| !range.contains(derived.base))
|
||
|
|
})
|
||
|
|
.expect("a deterministic test secret with a non-overlapping default");
|
||
|
|
(name, secret)
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn colliding_derived_defaults_are_blocked_instead_of_locally_renumbered() {
|
||
|
|
use std::collections::HashMap;
|
||
|
|
use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret};
|
||
|
|
let secret = NetworkSecret::from_bytes([19; 32]).unwrap();
|
||
|
|
let mut seen = HashMap::new();
|
||
|
|
let (first, second, range) = (0..=65_536)
|
||
|
|
.find_map(|index| {
|
||
|
|
let name = NetworkName::new(format!("derived-collision-{index}")).unwrap();
|
||
|
|
let range = derived_ipv4_range(NetworkKeys::derive(&name, &secret).network_id());
|
||
|
|
seen.insert(range.base, name.clone())
|
||
|
|
.map(|first| (first, name, range))
|
||
|
|
})
|
||
|
|
.expect("more network names than possible /24 ranges");
|
||
|
|
let agent = TestAgent::spawn(&SharedMemoryDiscovery::new())
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let first = agent.agent.join_network(&first, &secret).await.unwrap();
|
||
|
|
let second = agent.agent.join_network(&second, &secret).await.unwrap();
|
||
|
|
assert!(agent.agent.is_active(first).await);
|
||
|
|
assert!(!agent.agent.is_active(second).await);
|
||
|
|
let status = agent.agent.status().await.unwrap();
|
||
|
|
let blocked = status.network(&second).unwrap();
|
||
|
|
assert_eq!(blocked.range, Some(range));
|
||
|
|
assert_eq!(blocked.range_conflicts[0].network_id, first);
|
||
|
|
assert_eq!(
|
||
|
|
agent.agent.switch_network(second).await.unwrap(),
|
||
|
|
vec![first]
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
agent.agent.network_status(second).await.unwrap().range,
|
||
|
|
Some(range)
|
||
|
|
);
|
||
|
|
agent.agent.shutdown().await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn isolated_first_members_choose_the_same_range_and_keep_it_after_restart() {
|
||
|
|
let discovery = SharedMemoryDiscovery::new();
|
||
|
|
let (name, secret) = network("derived-first");
|
||
|
|
let a = TestAgent::spawn(&discovery).await.unwrap();
|
||
|
|
let b = TestAgent::spawn(&SharedMemoryDiscovery::new())
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let id = a.agent.join_network(&name, &secret).await.unwrap();
|
||
|
|
b.agent.join_network(&name, &secret).await.unwrap();
|
||
|
|
let expected = derived_ipv4_range(id);
|
||
|
|
for agent in [&a.agent, &b.agent] {
|
||
|
|
wait_until("isolated member allocates in its derived range", || async {
|
||
|
|
let status = agent.network_status(id).await.ok()?;
|
||
|
|
(status.range == Some(expected) && !status.members.is_empty()).then_some(())
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
}
|
||
|
|
let dir = a.stop().await;
|
||
|
|
let a = Agent::spawn(config_with(dir.path(), &discovery))
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(a.network_status(id).await.unwrap().range, Some(expected));
|
||
|
|
a.shutdown().await;
|
||
|
|
b.agent.shutdown().await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn overlap_blocks_activation_switches_and_resumes_across_restart() {
|
||
|
|
let discovery = SharedMemoryDiscovery::new();
|
||
|
|
let a = TestAgent::spawn_with(
|
||
|
|
|c| c.with_overlay_ipv4_range(Some(DEFAULT_IPV4_RANGE)),
|
||
|
|
&discovery,
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let (one_name, one_secret) = network("conflict-one");
|
||
|
|
let (two_name, two_secret) = network("conflict-two");
|
||
|
|
let one = a.agent.join_network(&one_name, &one_secret).await.unwrap();
|
||
|
|
a.agent.network_status(one).await.unwrap();
|
||
|
|
let two = a.agent.join_network(&two_name, &two_secret).await.unwrap();
|
||
|
|
assert!(a.agent.is_active(one).await);
|
||
|
|
assert!(!a.agent.is_active(two).await);
|
||
|
|
assert!(matches!(
|
||
|
|
a.agent.activate_network(two).await,
|
||
|
|
Err(tsunagi::Error::NetworkRangeConflict { .. })
|
||
|
|
));
|
||
|
|
let status = a.agent.status().await.unwrap();
|
||
|
|
let blocked = status.network(&two).unwrap();
|
||
|
|
assert_eq!(blocked.range, Some(DEFAULT_IPV4_RANGE));
|
||
|
|
assert_eq!(blocked.range_conflicts[0].network_id, one);
|
||
|
|
assert_eq!(blocked.range_conflicts[0].name, one_name);
|
||
|
|
assert_eq!(a.agent.switch_network(two).await.unwrap(), vec![one]);
|
||
|
|
assert!(a.agent.is_active(two).await);
|
||
|
|
assert!(!a.agent.is_active(one).await);
|
||
|
|
|
||
|
|
a.agent.network_status(two).await.unwrap();
|
||
|
|
let dir = a.stop().await;
|
||
|
|
let a = Agent::spawn(config_with(dir.path(), &discovery))
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
assert!(a.is_active(two).await, "the user's choice survives restart");
|
||
|
|
assert!(!a.is_active(one).await);
|
||
|
|
a.deactivate_network(two).await.unwrap();
|
||
|
|
assert!(
|
||
|
|
a.is_active(one).await,
|
||
|
|
"suspended networks resume when the blocker stops"
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
a.network_status(one).await.unwrap().range,
|
||
|
|
Some(DEFAULT_IPV4_RANGE),
|
||
|
|
"legacy ranges survive the new default"
|
||
|
|
);
|
||
|
|
a.shutdown().await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn late_authenticated_state_with_a_nested_range_suspends_only_that_network() {
|
||
|
|
let discovery = SharedMemoryDiscovery::new();
|
||
|
|
let broad: Ipv4Range = "10.90.0.0/16".parse().unwrap();
|
||
|
|
let narrow: Ipv4Range = "10.90.12.0/24".parse().unwrap();
|
||
|
|
let (name, secret) = network_outside("late-conflict", &[broad]);
|
||
|
|
let remote = TestAgent::spawn_with(|c| c.with_overlay_ipv4_range(Some(narrow)), &discovery)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let target = remote.agent.join_network(&name, &secret).await.unwrap();
|
||
|
|
wait_until("remote range is persisted", || async {
|
||
|
|
remote
|
||
|
|
.agent
|
||
|
|
.network_status(target)
|
||
|
|
.await
|
||
|
|
.ok()?
|
||
|
|
.members
|
||
|
|
.first()
|
||
|
|
.map(|_| ())
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
|
||
|
|
let (other_name, other_secret) = network("existing-broad");
|
||
|
|
// Seed an existing network using a real agent and its signed claim.
|
||
|
|
let seeded = TestAgent::spawn_with(
|
||
|
|
|c| c.with_overlay_ipv4_range(Some(broad)),
|
||
|
|
&SharedMemoryDiscovery::new(),
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let other = seeded
|
||
|
|
.agent
|
||
|
|
.join_network(&other_name, &other_secret)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
wait_until("broad range is persisted", || async {
|
||
|
|
seeded
|
||
|
|
.agent
|
||
|
|
.network_status(other)
|
||
|
|
.await
|
||
|
|
.ok()?
|
||
|
|
.members
|
||
|
|
.first()
|
||
|
|
.map(|_| ())
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
let dir = seeded.stop().await;
|
||
|
|
let seeded = Agent::spawn(config_with(dir.path(), &discovery))
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
seeded.join_network(&name, &secret).await.unwrap();
|
||
|
|
wait_until("late overlap suspends the joining network", || async {
|
||
|
|
let status = seeded.status().await.ok()?;
|
||
|
|
let blocked = status.network(&target)?;
|
||
|
|
(!seeded.is_active(target).await
|
||
|
|
&& blocked
|
||
|
|
.range_conflicts
|
||
|
|
.iter()
|
||
|
|
.any(|c| c.network_id == other))
|
||
|
|
.then_some(())
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
assert!(seeded.is_active(other).await);
|
||
|
|
assert_eq!(seeded.switch_network(target).await.unwrap(), vec![other]);
|
||
|
|
assert_eq!(
|
||
|
|
seeded.network_status(target).await.unwrap().range,
|
||
|
|
Some(narrow)
|
||
|
|
);
|
||
|
|
seeded.shutdown().await;
|
||
|
|
remote.agent.shutdown().await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn a_wide_range_suspends_all_overlaps_and_manual_stop_cancels_resume() {
|
||
|
|
let discovery = SharedMemoryDiscovery::new();
|
||
|
|
let hub = TestAgent::spawn(&discovery).await.unwrap();
|
||
|
|
let mut members = Vec::new();
|
||
|
|
let mut ids = Vec::new();
|
||
|
|
for (name, range) in [
|
||
|
|
("narrow-one", "10.81.0.0/24"),
|
||
|
|
("narrow-two", "10.81.1.0/24"),
|
||
|
|
] {
|
||
|
|
let peer = TestAgent::spawn_with(
|
||
|
|
|c| c.with_overlay_ipv4_range(Some(range.parse().unwrap())),
|
||
|
|
&discovery,
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let (name, secret) = network_outside(
|
||
|
|
name,
|
||
|
|
&[
|
||
|
|
"10.81.0.0/24".parse().unwrap(),
|
||
|
|
"10.81.1.0/24".parse().unwrap(),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
let id = peer.agent.join_network(&name, &secret).await.unwrap();
|
||
|
|
peer.agent.network_status(id).await.unwrap();
|
||
|
|
hub.agent.join_network(&name, &secret).await.unwrap();
|
||
|
|
wait_until("hub adopts the established narrow range", || async {
|
||
|
|
(hub.agent.network_status(id).await.ok()?.range == Some(range.parse().unwrap()))
|
||
|
|
.then_some(())
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
members.push(peer);
|
||
|
|
ids.push(id);
|
||
|
|
}
|
||
|
|
let (name, secret) = network_outside(
|
||
|
|
"wide-overlap",
|
||
|
|
&[
|
||
|
|
"10.81.0.0/24".parse().unwrap(),
|
||
|
|
"10.81.1.0/24".parse().unwrap(),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
let wide = TestAgent::spawn_with(
|
||
|
|
|c| c.with_overlay_ipv4_range(Some("10.0.0.0/8".parse().unwrap())),
|
||
|
|
&discovery,
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let id = wide.agent.join_network(&name, &secret).await.unwrap();
|
||
|
|
wide.agent.network_status(id).await.unwrap();
|
||
|
|
hub.agent.join_network(&name, &secret).await.unwrap();
|
||
|
|
wait_until("wide range is blocked by both narrow networks", || async {
|
||
|
|
let status = hub.agent.status().await.ok()?;
|
||
|
|
let blocked = status.network(&id)?;
|
||
|
|
(!hub.agent.is_active(id).await && blocked.range_conflicts.len() == 2).then_some(())
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
let stopped = hub.agent.switch_network(id).await.unwrap();
|
||
|
|
assert_eq!(stopped.len(), 2);
|
||
|
|
assert!(ids.iter().all(|id| stopped.contains(id)));
|
||
|
|
assert_eq!(hub.agent.switch_network(ids[0]).await.unwrap(), vec![id]);
|
||
|
|
assert!(
|
||
|
|
hub.agent.is_active(ids[1]).await,
|
||
|
|
"switching away from the wide range frees the other narrow range too"
|
||
|
|
);
|
||
|
|
assert_eq!(hub.agent.switch_network(id).await.unwrap().len(), 2);
|
||
|
|
hub.agent.pause_network(ids[0]).await.unwrap();
|
||
|
|
hub.agent.deactivate_network(id).await.unwrap();
|
||
|
|
assert!(
|
||
|
|
!hub.agent.is_active(ids[0]).await,
|
||
|
|
"explicitly stopped networks do not resume"
|
||
|
|
);
|
||
|
|
assert!(hub.agent.is_active(ids[1]).await);
|
||
|
|
assert!(
|
||
|
|
hub.agent
|
||
|
|
.switch_network(tsunagi::NetworkId::from_bytes([0; 32]))
|
||
|
|
.await
|
||
|
|
.is_err()
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
hub.agent.is_active(ids[1]).await,
|
||
|
|
"unknown selectors do not stop any network"
|
||
|
|
);
|
||
|
|
hub.agent.shutdown().await;
|
||
|
|
wide.agent.shutdown().await;
|
||
|
|
for peer in members {
|
||
|
|
peer.agent.shutdown().await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn simultaneous_switches_never_leave_overlapping_networks_active() {
|
||
|
|
let discovery = SharedMemoryDiscovery::new();
|
||
|
|
let agent = TestAgent::spawn_with(
|
||
|
|
|c| c.with_overlay_ipv4_range(Some(DEFAULT_IPV4_RANGE)),
|
||
|
|
&discovery,
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let (a, a_secret) = network("switch-a");
|
||
|
|
let (b, b_secret) = network("switch-b");
|
||
|
|
let a = agent.agent.join_network(&a, &a_secret).await.unwrap();
|
||
|
|
agent.agent.network_status(a).await.unwrap();
|
||
|
|
let b = agent.agent.join_network(&b, &b_secret).await.unwrap();
|
||
|
|
let (left, right) = tokio::join!(agent.agent.switch_network(b), agent.agent.switch_network(a));
|
||
|
|
left.unwrap();
|
||
|
|
right.unwrap();
|
||
|
|
assert_ne!(
|
||
|
|
agent.agent.is_active(a).await,
|
||
|
|
agent.agent.is_active(b).await
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
agent
|
||
|
|
.agent
|
||
|
|
.list_networks()
|
||
|
|
.await
|
||
|
|
.unwrap()
|
||
|
|
.iter()
|
||
|
|
.filter(|n| n.active)
|
||
|
|
.count(),
|
||
|
|
1
|
||
|
|
);
|
||
|
|
agent.agent.shutdown().await;
|
||
|
|
}
|