use std::{
collections::HashMap,
net::IpAddr,
sync::{
Arc, RwLock,
atomic::{AtomicUsize, Ordering},
},
time::{Duration, SystemTime},
};
use anyhow::Context;
use sciparse::{
address::ip_socket_addr::ScionSocketIpAddr,
path::{ScionPath, fingerprint::data_plane::DpPathFingerprint},
};
use tokio_util::sync::CancellationToken;
use crate::pg_wap2::{
paths::{GrantWatch, PathManager, PathSegmentsGuard, UsedPath},
sni::WapSNI,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UplinkKey {
pub path_fp: DpPathFingerprint,
pub wag: ScionSocketIpAddr,
}
pub struct UplinkManager<Establisher: UplinkEstablisher>(Arc<UplinkManagerInner<Establisher>>);
impl<Establisher: UplinkEstablisher> Clone for UplinkManager<Establisher> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
struct UplinkManagerInner<Establisher: UplinkEstablisher> {
uplinks: RwLock<HashMap<UplinkKey, Arc<UplinkEntry<Establisher::Uplink>>>>,
establisher: Establisher,
paths: PathManager,
min_path_lifetime: Duration,
maintenance_interval: Duration,
}
impl<Establisher: UplinkEstablisher> UplinkManager<Establisher> {
pub fn new(
establisher: Establisher,
paths: PathManager,
min_path_lifetime: Duration,
maintenance_interval: Duration,
) -> Self {
Self(Arc::new(UplinkManagerInner {
uplinks: RwLock::new(HashMap::new()),
establisher,
paths,
min_path_lifetime,
maintenance_interval,
}))
}
pub async fn establish_stream(
&self,
client_ip: IpAddr,
sni: &WapSNI,
used_path: UsedPath,
wag: ScionSocketIpAddr,
now: SystemTime,
) -> anyhow::Result<UplinkStreamGuard<Establisher::Uplink>> {
let grants = self
.0
.paths
.watch_grants(client_ip, sni.customer_domain(), &used_path, now)
.context("Client is not authorized to use the path")?;
let reservation = self.reserve_uplink(used_path, wag, now).await?;
let stream = reservation
.entry()
.uplink
.establish_stream(sni)
.await
.context("Failed to establish stream on the uplink")?;
Ok(reservation.attach(stream, grants))
}
async fn reserve_uplink(
&self,
used_path: UsedPath,
wag: ScionSocketIpAddr,
now: SystemTime,
) -> anyhow::Result<UplinkReservation<Establisher::Uplink>> {
let key = UplinkKey {
path_fp: used_path.path.fingerprint(),
wag,
};
{
let uplinks = self.0.uplinks.read().unwrap();
if let Some(entry) = uplinks.get(&key).filter(|entry| !entry.is_closed()) {
return Ok(UplinkReservation::new(entry.clone()));
}
}
let segments_guard = self
.0
.paths
.hold_segments(&used_path, now)
.await
.context("Failed to guard the segments of the uplink path")?;
let closed = CancellationToken::new();
let uplink = self
.0
.establisher
.establish_connection(used_path.path.clone(), wag, closed.clone())
.await
.context("Failed to establish uplink")?;
let entry = Arc::new(UplinkEntry {
uplink,
established: now,
used_path: RwLock::new(used_path),
_segments_guard: segments_guard,
active_stream_count: AtomicUsize::new(0),
closed,
});
let mut replaced = None;
let mut uplinks = self.0.uplinks.write().unwrap();
let entry = match uplinks.entry(key) {
std::collections::hash_map::Entry::Occupied(mut occupied)
if occupied.get().is_closed() =>
{
tracing::debug!(?key, "Replacing a closed uplink");
replaced = Some(occupied.insert(entry.clone()));
entry
}
std::collections::hash_map::Entry::Occupied(occupied) => {
tracing::debug!(?key, "Lost the race to establish an uplink, dropping ours");
occupied.get().clone()
}
std::collections::hash_map::Entry::Vacant(vacant) => {
tracing::debug!(?key, "Established a new uplink");
vacant.insert(entry).clone()
}
};
let reservation = UplinkReservation::new(entry);
drop(uplinks);
drop(replaced);
Ok(reservation)
}
pub async fn run(&self) {
loop {
self.maintain(SystemTime::now()).await;
tokio::time::sleep(self.0.maintenance_interval).await;
}
}
pub async fn maintain(&self, now: SystemTime) {
let uplinks: Vec<(UplinkKey, Arc<UplinkEntry<Establisher::Uplink>>)> = self
.0
.uplinks
.read()
.unwrap()
.iter()
.map(|(key, entry)| (*key, entry.clone()))
.collect();
for (key, entry) in uplinks {
if entry.is_closed() {
continue;
}
let used_path = entry.used_path();
let Some(expiry) = used_path.expiration() else {
continue;
};
if expiry > now + self.0.min_path_lifetime {
continue;
}
self.refresh_uplink_path(key, &entry, &used_path, now).await;
}
self.reap();
}
fn reap(&self) {
let mut reaped = Vec::new();
{
let mut uplinks = self.0.uplinks.write().unwrap();
uplinks.retain(|key, entry| {
let keep =
!entry.is_closed() && entry.active_stream_count.load(Ordering::Relaxed) > 0;
if !keep {
tracing::debug!(
?key,
closed = entry.is_closed(),
"Removing uplink from the manager"
);
entry.closed.cancel();
reaped.push(entry.clone());
}
keep
});
}
drop(reaped);
}
async fn refresh_uplink_path(
&self,
key: UplinkKey,
entry: &UplinkEntry<Establisher::Uplink>,
used_path: &UsedPath,
now: SystemTime,
) {
let refreshed = match self.0.paths.refresh_path(used_path, now).await {
Ok(refreshed) => refreshed,
Err(e) => {
tracing::warn!(
?key,
?e,
"Failed to refresh path of uplink, keeping old version"
);
return;
}
};
if refreshed.expiration() <= used_path.expiration() {
tracing::debug!(
?key,
"Refreshed path does not live longer, keeping the old one"
);
return;
}
if refreshed.path.fingerprint() != key.path_fp {
debug_assert!(
false,
"Refreshed path has a different fingerprint, this should not happen"
);
tracing::warn!(
?key,
"Refreshed path has a different fingerprint, keeping the old one"
);
return;
}
if let Err(e) = entry.uplink.replace_path(refreshed.path.clone()) {
tracing::warn!(?key, "Uplink rejected the refreshed path: {e:#}");
return;
}
tracing::debug!(?key, expiry = ?refreshed.expiration(), "Refreshed the path of an uplink");
*entry.used_path.write().unwrap() = refreshed;
}
}
pub struct UplinkEntry<UplinkType: GenericUplink> {
uplink: UplinkType,
established: SystemTime,
used_path: RwLock<UsedPath>,
_segments_guard: PathSegmentsGuard,
active_stream_count: AtomicUsize,
closed: CancellationToken,
}
impl<UplinkType: GenericUplink> UplinkEntry<UplinkType> {
pub fn used_path(&self) -> UsedPath {
self.used_path.read().unwrap().clone()
}
pub fn established(&self) -> SystemTime {
self.established
}
pub fn is_closed(&self) -> bool {
self.closed.is_cancelled()
}
pub async fn closed(&self) {
self.closed.cancelled().await;
}
pub fn uplink(&self) -> &UplinkType {
&self.uplink
}
fn release_stream(&self) {
let previous = self.active_stream_count.fetch_sub(1, Ordering::Relaxed);
debug_assert!(previous > 0, "released a stream that was never reserved");
}
}
impl<UplinkType: GenericUplink> Drop for UplinkEntry<UplinkType> {
fn drop(&mut self) {
self.closed.cancel();
}
}
struct UplinkReservation<UplinkType: GenericUplink>(Option<Arc<UplinkEntry<UplinkType>>>);
impl<UplinkType: GenericUplink> UplinkReservation<UplinkType> {
fn new(entry: Arc<UplinkEntry<UplinkType>>) -> Self {
entry.active_stream_count.fetch_add(1, Ordering::Relaxed);
Self(Some(entry))
}
fn entry(&self) -> &Arc<UplinkEntry<UplinkType>> {
self.0
.as_ref()
.expect("the reservation is only taken by `attach`, which consumes self")
}
fn attach(
mut self,
stream: UplinkType::StreamType,
grants: GrantWatch,
) -> UplinkStreamGuard<UplinkType> {
UplinkStreamGuard {
uplink_entry: self.0.take().expect("attach consumes self"),
stream: Some(stream),
grants,
}
}
}
impl<UplinkType: GenericUplink> Drop for UplinkReservation<UplinkType> {
fn drop(&mut self) {
if let Some(entry) = self.0.take() {
entry.release_stream();
}
}
}
pub struct UplinkStreamGuard<UplinkType: GenericUplink> {
uplink_entry: Arc<UplinkEntry<UplinkType>>,
stream: Option<UplinkType::StreamType>,
grants: GrantWatch,
}
impl<UplinkType: GenericUplink> UplinkStreamGuard<UplinkType> {
pub async fn grant_expired(&self) {
self.grants.expired().await;
}
pub async fn uplink_closed(&self) {
self.uplink_entry.closed().await;
}
pub fn take_stream(&mut self) -> Option<UplinkType::StreamType> {
self.stream.take()
}
pub fn uplink_entry(&self) -> &Arc<UplinkEntry<UplinkType>> {
&self.uplink_entry
}
}
impl<UplinkType: GenericUplink> Drop for UplinkStreamGuard<UplinkType> {
fn drop(&mut self) {
self.uplink_entry.release_stream();
}
}
#[async_trait::async_trait]
pub trait UplinkEstablisher: Send + Sync + 'static {
type Uplink: GenericUplink;
async fn establish_connection(
&self,
path: ScionPath,
dst_addr: ScionSocketIpAddr,
closed: CancellationToken,
) -> anyhow::Result<Self::Uplink>;
}
#[async_trait::async_trait]
pub trait GenericUplink: Send + Sync + 'static {
type StreamType: Send;
async fn establish_stream(&self, dst_sni: &WapSNI) -> anyhow::Result<Self::StreamType>;
fn replace_path(&self, new_path: ScionPath) -> anyhow::Result<()>;
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use futures::FutureExt;
use super::*;
use crate::pg_wap2::{
paths::UsedPath,
test_util::{
Fixture, IDLE_EVICTION_TIME, MockFetcher, at, client_ip, core_ia, granted_id, leaf_ia,
other_client_ip, other_sni, sni, stranger_ip, up_segment, wag,
},
};
const ALWAYS_REPATH: Duration = Duration::from_secs(30 * 24 * 3600);
const NEVER_REPATH: Duration = Duration::from_secs(60);
#[derive(Clone, Default)]
struct TestUplinks(Arc<Mutex<TestUplinkState>>);
#[derive(Default)]
struct TestUplinkState {
log: Vec<UplinkEvent>,
close_tokens: Vec<CancellationToken>,
failing: Failures,
}
#[derive(Default)]
struct Failures {
connect: bool,
stream: bool,
replace_path: bool,
}
#[derive(Debug, PartialEq, Eq)]
enum UplinkEvent {
Connected(ScionSocketIpAddr),
ConnectionRefused,
Stream(String),
StreamRefused,
PathReplaced,
PathReplacementRejected,
}
impl TestUplinks {
fn with_state<T>(&self, f: impl FnOnce(&mut TestUplinkState) -> T) -> T {
f(&mut self.0.lock().unwrap())
}
fn fail_connections(&self, fail: bool) {
self.with_state(|state| state.failing.connect = fail);
}
fn fail_streams(&self, fail: bool) {
self.with_state(|state| state.failing.stream = fail);
}
fn fail_path_replacements(&self, fail: bool) {
self.with_state(|state| state.failing.replace_path = fail);
}
fn close(&self, index: usize) {
self.with_state(|state| state.close_tokens[index].cancel());
}
fn is_closed(&self, index: usize) -> bool {
self.with_state(|state| state.close_tokens[index].is_cancelled())
}
fn count_events(&self, matching: impl Fn(&UplinkEvent) -> bool) -> usize {
self.with_state(|state| state.log.iter().filter(|event| matching(event)).count())
}
fn path_replacements(&self) -> usize {
self.count_events(|event| matches!(event, UplinkEvent::PathReplaced))
}
fn path_replacement_attempts(&self) -> usize {
self.count_events(|event| {
matches!(
event,
UplinkEvent::PathReplaced | UplinkEvent::PathReplacementRejected
)
})
}
fn connections(&self) -> usize {
self.count_events(|event| matches!(event, UplinkEvent::Connected(_)))
}
fn connection_attempts(&self) -> usize {
self.count_events(|event| {
matches!(
event,
UplinkEvent::Connected(_) | UplinkEvent::ConnectionRefused
)
})
}
fn stream_attempts(&self) -> usize {
self.count_events(|event| {
matches!(event, UplinkEvent::Stream(_) | UplinkEvent::StreamRefused)
})
}
fn streamed_snis(&self) -> Vec<String> {
self.with_state(|state| {
state
.log
.iter()
.filter_map(|event| {
match event {
UplinkEvent::Stream(sni) => Some(sni.clone()),
_ => None,
}
})
.collect()
})
}
}
#[async_trait::async_trait]
impl UplinkEstablisher for TestUplinks {
type Uplink = TestUplink;
async fn establish_connection(
&self,
_path: ScionPath,
dst_addr: ScionSocketIpAddr,
closed: CancellationToken,
) -> anyhow::Result<Self::Uplink> {
self.with_state(|state| {
if state.failing.connect {
state.log.push(UplinkEvent::ConnectionRefused);
anyhow::bail!("the test asked connecting to fail");
}
state.log.push(UplinkEvent::Connected(dst_addr));
state.close_tokens.push(closed);
Ok(TestUplink(self.clone()))
})
}
}
struct TestUplink(TestUplinks);
#[async_trait::async_trait]
impl GenericUplink for TestUplink {
type StreamType = ();
async fn establish_stream(&self, dst_sni: &WapSNI) -> anyhow::Result<Self::StreamType> {
self.0.with_state(|state| {
if state.failing.stream {
state.log.push(UplinkEvent::StreamRefused);
anyhow::bail!("the test asked streaming to fail");
}
state
.log
.push(UplinkEvent::Stream(dst_sni.full_domain().to_string()));
Ok(())
})
}
fn replace_path(&self, _new_path: ScionPath) -> anyhow::Result<()> {
self.0.with_state(|state| {
if state.failing.replace_path {
state.log.push(UplinkEvent::PathReplacementRejected);
anyhow::bail!("the test asked path replacement to fail");
}
state.log.push(UplinkEvent::PathReplaced);
Ok(())
})
}
}
type Harness = (Fixture, TestUplinks, UplinkManager<TestUplinks>, UsedPath);
fn manager_for(
fixture: &Fixture,
uplinks: TestUplinks,
min_path_lifetime: Duration,
) -> UplinkManager<TestUplinks> {
UplinkManager::new(
uplinks,
fixture.paths.clone(),
min_path_lifetime,
Duration::from_secs(10),
)
}
async fn uplink_fixture() -> Harness {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
fixture.grant_non_core(Vec::new(), at(0));
let uplinks = TestUplinks::default();
let manager = manager_for(&fixture, uplinks.clone(), NEVER_REPATH);
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), leaf_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("an AS local path always exists");
(fixture, uplinks, manager, used)
}
async fn public_path_fixture(min_path_lifetime: Duration) -> Harness {
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up_segment(0)]),
Duration::from_secs(10 * 24 * 3600),
);
fixture.grant_non_core(Vec::new(), at(0));
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the public segment yields a path");
let uplinks = TestUplinks::default();
let manager = manager_for(&fixture, uplinks.clone(), min_path_lifetime);
(fixture, uplinks, manager, used)
}
async fn granted_path_fixture(min_path_lifetime: Duration, auth_duration: Duration) -> Harness {
let fixture = Fixture::new(MockFetcher::empty(), auth_duration);
fixture.grant_non_core(vec![up_segment(0)], at(0));
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the granted segment yields a path");
let uplinks = TestUplinks::default();
let manager = manager_for(&fixture, uplinks.clone(), min_path_lifetime);
(fixture, uplinks, manager, used)
}
#[tokio::test]
async fn uplinks_are_shared_per_path_and_wag() {
let (fixture, uplinks, manager, used) = uplink_fixture().await;
fixture.grant_target(client_ip(), other_sni().customer_domain(), at(0));
let first = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
.await
.expect("the first stream is established");
let second = manager
.establish_stream(
client_ip(),
&other_sni(),
used.clone(),
wag(leaf_ia()),
at(0),
)
.await
.expect("the second stream is established");
assert_eq!(
uplinks.connections(),
1,
"the same (path, WAG) pair must share one uplink"
);
assert_eq!(
uplinks.streamed_snis(),
vec![sni().to_string(), other_sni().to_string()],
"both SNIs get their own stream on that uplink"
);
assert!(std::ptr::eq(
Arc::as_ptr(first.uplink_entry()),
Arc::as_ptr(second.uplink_entry())
));
let other_wag = manager
.establish_stream(client_ip(), &sni(), used, wag(core_ia()), at(0))
.await
.expect("the stream to the other WAG is established");
assert_eq!(uplinks.connections(), 2);
assert!(!std::ptr::eq(
Arc::as_ptr(first.uplink_entry()),
Arc::as_ptr(other_wag.uplink_entry())
));
}
#[tokio::test]
async fn uplinks_without_streams_are_reaped() {
let (_fixture, uplinks, manager, used) = uplink_fixture().await;
let stream = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
.await
.expect("the stream is established");
manager.maintain(at(0)).await;
let second = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
.await
.expect("the second stream is established");
assert_eq!(uplinks.connections(), 1);
drop(stream);
drop(second);
manager.maintain(at(0)).await;
manager
.establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
.await
.expect("a new stream is established");
assert_eq!(
uplinks.connections(),
2,
"the reaped uplink has to be established again"
);
}
async fn shared_uplink_fixture() -> Harness {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
fixture.grant_non_core(vec![up_segment(0)], at(0));
fixture.grant_non_core_to(other_client_ip(), vec![up_segment(0)], at(0));
let uplinks = TestUplinks::default();
let manager = manager_for(&fixture, uplinks.clone(), ALWAYS_REPATH);
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the granted segment yields a path");
(fixture, uplinks, manager, used)
}
#[tokio::test]
async fn a_shared_uplink_outlives_the_grant_of_the_client_that_established_it() {
let (fixture, uplinks, manager, used) = shared_uplink_fixture().await;
let first = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(core_ia()), at(0))
.await
.expect("the first client's stream is established");
let second = manager
.establish_stream(
other_client_ip(),
&sni(),
used.clone(),
wag(core_ia()),
at(0),
)
.await
.expect("the second client's stream is established");
assert_eq!(uplinks.connections(), 1, "both clients share one uplink");
fixture.grant_non_core_to(other_client_ip(), vec![up_segment(600)], at(90));
fixture.auth.clean(at(101));
assert!(
fixture
.auth
.segment_grant_expiry(
client_ip(),
sni().customer_domain(),
&granted_id(&up_segment(0)),
at(101)
)
.is_none()
);
assert!(
fixture
.auth
.segment(&granted_id(&up_segment(0)), at(101))
.is_some(),
"a segment stays available while any client is granted it"
);
assert!(
first.grant_expired().now_or_never().is_some(),
"the client whose grant lapsed must observe it"
);
assert!(
second.grant_expired().now_or_never().is_none(),
"the client that refreshed must keep its stream"
);
manager.maintain(at(101)).await;
assert_eq!(
uplinks.path_replacements(),
1,
"the uplink is re-pathed off a grant it was not established with"
);
assert_eq!(
second.uplink_entry().used_path().expiration(),
used.expiration().map(|old| old + Duration::from_secs(600))
);
assert_eq!(
uplinks.connections(),
1,
"re-pathing must not reconnect the uplink"
);
}
#[tokio::test]
async fn a_stream_is_refused_without_a_grant_for_the_paths_segments() {
let (fixture, uplinks, manager, used) = shared_uplink_fixture().await;
fixture.grant_target(stranger_ip(), sni().customer_domain(), at(0));
assert!(
manager
.establish_stream(stranger_ip(), &sni(), used, wag(core_ia()), at(0))
.await
.is_err(),
"a client without a grant on the path's segments must not get a stream on it"
);
assert_eq!(
uplinks.connection_attempts(),
0,
"and no uplink is established for it"
);
}
#[tokio::test]
async fn a_stream_is_refused_for_an_ip_without_any_authorization() {
let (fixture, uplinks, manager, used) = uplink_fixture().await;
assert!(!fixture.auth.ip_is_authorized(stranger_ip(), at(0)));
assert!(
manager
.establish_stream(stranger_ip(), &sni(), used, wag(leaf_ia()), at(0))
.await
.is_err(),
"an unauthenticated client must not get a stream"
);
assert_eq!(
uplinks.connection_attempts(),
0,
"and must not cause any dataplane work"
);
}
#[tokio::test]
async fn a_stream_is_refused_once_the_clients_grant_has_expired() {
let (fixture, uplinks, manager, used) = uplink_fixture().await;
assert!(used.segments.is_empty());
assert!(
manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(101))
.await
.is_err(),
"a lapsed grant does not authorize a stream, swept or not"
);
fixture.auth.clean(at(101));
assert!(
manager
.establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(101))
.await
.is_err(),
"and still does not once it has been swept"
);
assert_eq!(
uplinks.connection_attempts(),
0,
"an unauthorized client must not cause any dataplane work"
);
}
#[tokio::test]
async fn an_uplink_is_reused_by_a_stream_that_arrives_before_the_reaper() {
let (_fixture, uplinks, manager, used) = uplink_fixture().await;
let stream = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
.await
.expect("the stream is established");
let entry = stream.uplink_entry().clone();
drop(stream);
let second = manager
.establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
.await
.expect("the second stream is established");
assert_eq!(
uplinks.connections(),
1,
"an idle uplink is reused rather than replaced"
);
assert!(std::ptr::eq(
Arc::as_ptr(&entry),
Arc::as_ptr(second.uplink_entry())
));
manager.maintain(at(0)).await;
assert!(
!second.uplink_entry().is_closed(),
"an uplink that was picked up again must survive the reaper"
);
}
#[tokio::test]
async fn a_closed_uplink_tears_down_its_streams_and_is_not_reused() {
let (_fixture, uplinks, manager, used) = uplink_fixture().await;
let stream = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
.await
.expect("the stream is established");
assert_eq!(uplinks.connections(), 1);
assert!(stream.uplink_closed().now_or_never().is_none());
uplinks.close(0);
assert!(
stream.uplink_closed().now_or_never().is_some(),
"every stream on a closed uplink has to be told to tear down"
);
let replacement = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
.await
.expect("the stream is established on a new uplink");
assert_eq!(uplinks.connections(), 2, "the closed uplink is replaced");
assert!(!std::ptr::eq(
Arc::as_ptr(stream.uplink_entry()),
Arc::as_ptr(replacement.uplink_entry())
));
assert!(
!replacement.uplink_entry().is_closed(),
"the replacement is usable"
);
}
#[tokio::test]
async fn a_closed_uplink_is_reaped_even_though_it_still_has_streams() {
let (_fixture, uplinks, manager, used) = uplink_fixture().await;
let stream = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
.await
.expect("the stream is established");
uplinks.close(0);
manager.maintain(at(0)).await;
let replacement = manager
.establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
.await
.expect("the stream is established");
assert_eq!(
uplinks.connections(),
2,
"the reaped uplink has to be established again"
);
assert!(!std::ptr::eq(
Arc::as_ptr(stream.uplink_entry()),
Arc::as_ptr(replacement.uplink_entry())
));
}
#[tokio::test]
async fn reaping_an_unused_uplink_tells_its_holders_it_is_gone() {
let (_fixture, _uplinks, manager, used) = uplink_fixture().await;
let stream = manager
.establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
.await
.expect("the stream is established");
let entry = stream.uplink_entry().clone();
drop(stream);
manager.maintain(at(0)).await;
assert!(
entry.is_closed(),
"a reaped uplink counts as unusable for anything still holding it"
);
}
#[tokio::test]
async fn a_live_uplink_holds_the_segments_of_its_path() {
let (fixture, _uplinks, manager, used) = public_path_fixture(NEVER_REPATH).await;
let stream = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(core_ia()), at(0))
.await
.expect("the stream is established");
let held_at = at(121);
fixture.segments.maintain(held_at).await;
assert_eq!(
fixture.fetcher.calls(),
1,
"the guarded pair was never evicted, so it never had to be fetched again"
);
assert!(
fixture.paths.refresh_path(&used, held_at).await.is_ok(),
"the segments of a live uplink must stay resolvable"
);
drop(stream);
manager.maintain(held_at).await;
let idle_at = at(242);
fixture.segments.maintain(idle_at).await;
fixture
.segments
.segments(leaf_ia(), core_ia(), idle_at)
.await
.expect("segments are fetched again");
assert_eq!(
fixture.fetcher.calls(),
2,
"an unheld pair is evicted again once it goes idle"
);
}
#[tokio::test]
async fn uplinks_are_re_pathed_before_their_path_expires() {
let (fixture, uplinks, manager, used) =
granted_path_fixture(ALWAYS_REPATH, Duration::from_secs(10 * 24 * 3600)).await;
let expiry = used
.expiration()
.expect("a combined path has an expiration");
let stream = manager
.establish_stream(client_ip(), &sni(), used, wag(core_ia()), at(0))
.await
.expect("the stream is established");
manager.maintain(at(0)).await;
assert_eq!(uplinks.path_replacements(), 0);
fixture.grant_non_core(vec![up_segment(600)], at(0));
manager.maintain(at(0)).await;
assert_eq!(
uplinks.path_replacements(),
1,
"the uplink is given the newer path"
);
assert_eq!(
stream.uplink_entry().used_path().expiration(),
Some(expiry + Duration::from_secs(600))
);
assert_eq!(
uplinks.connections(),
1,
"re-pathing must not reconnect the uplink"
);
}
#[tokio::test]
async fn an_uplink_that_rejects_a_refreshed_path_keeps_the_one_it_has() {
let (fixture, uplinks, manager, used) =
granted_path_fixture(ALWAYS_REPATH, Duration::from_secs(10 * 24 * 3600)).await;
let expiry = used
.expiration()
.expect("a combined path has an expiration");
let stream = manager
.establish_stream(client_ip(), &sni(), used, wag(core_ia()), at(0))
.await
.expect("the stream is established");
fixture.grant_non_core(vec![up_segment(600)], at(0));
uplinks.fail_path_replacements(true);
manager.maintain(at(0)).await;
assert_eq!(
uplinks.path_replacement_attempts(),
1,
"the refresh was offered"
);
assert_eq!(uplinks.path_replacements(), 0, "and was rejected");
assert_eq!(
stream.uplink_entry().used_path().expiration(),
Some(expiry),
"the uplink must keep sending over the path it still has"
);
assert!(
!stream.uplink_entry().is_closed(),
"a rejected refresh is not fatal to the uplink"
);
uplinks.fail_path_replacements(false);
manager.maintain(at(0)).await;
assert_eq!(uplinks.path_replacements(), 1);
assert_eq!(
stream.uplink_entry().used_path().expiration(),
Some(expiry + Duration::from_secs(600))
);
}
#[tokio::test]
async fn an_uplink_whose_segments_are_gone_keeps_the_path_it_has() {
let (fixture, uplinks, manager, used) =
granted_path_fixture(ALWAYS_REPATH, Duration::from_secs(100)).await;
let expiry = used
.expiration()
.expect("a combined path has an expiration");
let stream = manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(core_ia()), at(0))
.await
.expect("the stream is established");
fixture.auth.clean(at(101));
assert!(fixture.paths.refresh_path(&used, at(101)).await.is_err());
manager.maintain(at(101)).await;
assert_eq!(
uplinks.path_replacement_attempts(),
0,
"there is no path to offer the uplink"
);
assert_eq!(
stream.uplink_entry().used_path().expiration(),
Some(expiry),
"a failed refresh leaves the uplink on its current path"
);
assert!(
!stream.uplink_entry().is_closed(),
"the uplink is only torn down once nothing streams over it"
);
}
#[tokio::test]
async fn a_failed_stream_releases_its_reservation_on_the_uplink() {
let (_fixture, uplinks, manager, used) = uplink_fixture().await;
uplinks.fail_streams(true);
assert!(
manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
.await
.is_err(),
"the client gets no stream if the uplink cannot open one"
);
assert_eq!(
uplinks.connections(),
1,
"the uplink itself was established before the stream failed"
);
assert_eq!(uplinks.stream_attempts(), 1);
manager.maintain(at(0)).await;
assert!(
uplinks.is_closed(0),
"an uplink whose only stream failed has to be reaped"
);
uplinks.fail_streams(false);
manager
.establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
.await
.expect("the next stream is established");
assert_eq!(
uplinks.connections(),
2,
"the reaped uplink had to be established again"
);
}
#[tokio::test]
async fn a_failed_uplink_releases_the_segments_of_its_path() {
let (fixture, uplinks, manager, used) = public_path_fixture(NEVER_REPATH).await;
uplinks.fail_connections(true);
assert!(
manager
.establish_stream(client_ip(), &sni(), used.clone(), wag(core_ia()), at(0))
.await
.is_err(),
"the client gets no stream if the uplink cannot be established"
);
assert_eq!(
uplinks.connection_attempts(),
1,
"the connection was attempted"
);
let idle_at = at(IDLE_EVICTION_TIME.as_secs() * 2 + 1);
fixture.segments.maintain(idle_at).await;
fixture
.segments
.segments(leaf_ia(), core_ia(), idle_at)
.await
.expect("segments are fetched again");
assert_eq!(
fixture.fetcher.calls(),
2,
"the pair of a failed uplink must not stay held"
);
uplinks.fail_connections(false);
manager
.establish_stream(client_ip(), &sni(), used, wag(core_ia()), idle_at)
.await
.expect("the next stream is established");
assert_eq!(uplinks.connections(), 1);
}
#[tokio::test]
async fn a_stream_is_refused_when_the_paths_segments_cannot_be_held() {
let (fixture, uplinks, manager, used) = public_path_fixture(NEVER_REPATH).await;
let evicted_at = at(IDLE_EVICTION_TIME.as_secs() + 1);
fixture.segments.maintain(evicted_at).await;
fixture.fetcher.set_failing(true);
assert!(
manager
.establish_stream(client_ip(), &sni(), used, wag(core_ia()), evicted_at)
.await
.is_err(),
"an uplink whose segments cannot be kept available must not be established"
);
assert_eq!(
uplinks.connection_attempts(),
0,
"the segments are secured before anything is connected"
);
}
}