Skip to main content

snap_control/pg_wap/
uplinks.rs

1// Copyright 2026 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Uplink management.
16//!
17//! One uplink is established per (path, WAG) pair and shared by every stream that wants to
18//! reach that WAG over that path.
19//!
20//! [`UplinkManager::maintain`] re-paths uplinks whose path is about to expire and cleans the ones
21//! that no stream is using anymore or that are closed.
22//!
23//! The dataplane is reached through two traits: [`UplinkEstablisher`] opens the connections, and
24//! [`GenericUplink`] is one established connection.
25//!
26//! An uplink is closed from either side: the uplink itself reports that it is no longer usable,
27//! e.g. after a network error, and the manager closes one it is dropping.
28//!
29//! Either way a closed uplink is not handed out again and its streams are torn down.
30//!
31//! The invariants we maintain are:
32//! - There is only one uplink per (path, WAG) pair.
33//! - Only a client which is authorized for the (path, SNI) can establish a stream on an uplink.
34//! - An uplink is closed when it signals it is no longer usable, or when the manager drops it.
35//! - An uplink will be closed when there are no streams using it anymore.
36//! - An uplink tries to refresh its path with the same segments before it expires, otherwise
37//!   closes.
38//!
39//! ## Authorization
40//!
41//! Uplinks are established for a certain path, which is built from public and granted segments.
42//! They are not directly tied to the authentication of the client that established them.
43//!
44//! As long as the authorized segments of at least one client are still valid, the uplink can use
45//! this segment.
46//!
47//! The uplink is closed when:
48//! a. no client is using it
49//! b. the segments used for the uplink can no longer be refreshed, e.g. because they are no longer
50//! granted to any client.
51//!
52//! The client's authentication is tracked through [`UplinkStreamGuard::grant_expired`] returned by
53//! [`UplinkManager::establish_stream`]. When this triggers, the client must tear down its stream.
54
55use std::{
56    collections::HashMap,
57    net::IpAddr,
58    sync::{
59        Arc, RwLock,
60        atomic::{AtomicUsize, Ordering},
61    },
62    time::{Duration, SystemTime},
63};
64
65use anyhow::Context;
66use http::StatusCode;
67use sciparse::{
68    address::ip_socket_addr::ScionSocketIpAddr,
69    path::{ScionPath, fingerprint::data_plane::DpPathFingerprint},
70};
71use serde::{Deserialize, Serialize};
72use serde_with::{DurationSeconds, serde_as};
73use tokio_util::sync::CancellationToken;
74
75use crate::pg_wap::{
76    paths::{GrantWatch, PathManager, PathSegmentsGuard, UsedPath},
77    sni::WapSNI,
78};
79
80/// Identifies an uplink by the (path, WAG) pair it was established for.
81///
82/// [`DpPathFingerprint`] covers the hops of the path but not their expiration times, so an
83/// uplink keeps its key when its path is refreshed from newer copies of the same segments.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub struct UplinkKey {
86    /// Fingerprint of the path the uplink sends over.
87    pub path_fp: DpPathFingerprint,
88    /// The WAG the uplink is connected to.
89    pub wag: ScionSocketIpAddr,
90}
91
92/// The timings an [`UplinkManager`] re-paths and reaps uplinks with.
93#[serde_as]
94#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
95#[serde(deny_unknown_fields)]
96pub struct UplinkManagerConfig {
97    /// An uplink whose path has less than this left is re-pathed by [`UplinkManager::maintain`].
98    #[serde_as(as = "DurationSeconds<u64>")]
99    #[serde(default = "UplinkManagerConfig::default_min_path_lifetime")]
100    pub min_path_lifetime: Duration,
101    /// Interval between two [`UplinkManager::maintain`] runs.
102    #[serde_as(as = "DurationSeconds<u64>")]
103    #[serde(default = "UplinkManagerConfig::default_maintenance_interval")]
104    pub maintenance_interval: Duration,
105}
106
107impl UplinkManagerConfig {
108    fn default_min_path_lifetime() -> Duration {
109        Duration::from_secs(60)
110    }
111
112    fn default_maintenance_interval() -> Duration {
113        Duration::from_secs(10)
114    }
115
116    /// Checks that the timings are usable.
117    ///
118    /// Returns an error if a duration is zero.
119    pub fn validate(&self) -> anyhow::Result<()> {
120        if self.min_path_lifetime == Duration::ZERO {
121            anyhow::bail!("min_path_lifetime must be greater than 0");
122        }
123
124        if self.maintenance_interval == Duration::ZERO {
125            anyhow::bail!("maintenance_interval must be greater than 0");
126        }
127
128        Ok(())
129    }
130}
131
132impl Default for UplinkManagerConfig {
133    fn default() -> Self {
134        Self {
135            min_path_lifetime: Self::default_min_path_lifetime(),
136            maintenance_interval: Self::default_maintenance_interval(),
137        }
138    }
139}
140
141/// Owns the uplinks towards the WAGs, one per (path, WAG) pair.
142///
143/// Cheap to clone; all clones share the same state.
144pub struct UplinkManager<Establisher: UplinkEstablisher>(Arc<UplinkManagerInner<Establisher>>);
145
146// Not derived: the derive would require `Establisher: Clone`, which it need not be.
147impl<Establisher: UplinkEstablisher> Clone for UplinkManager<Establisher> {
148    fn clone(&self) -> Self {
149        Self(self.0.clone())
150    }
151}
152
153/// The state shared by all clones of an [`UplinkManager`].
154struct UplinkManagerInner<Establisher: UplinkEstablisher> {
155    uplinks: RwLock<HashMap<UplinkKey, Arc<UplinkEntry<Establisher::Uplink>>>>,
156    /// Opens the connections to the WAGs.
157    establisher: Establisher,
158    paths: PathManager,
159    config: UplinkManagerConfig,
160}
161
162impl<Establisher: UplinkEstablisher> UplinkManager<Establisher> {
163    /// Creates a new, empty uplink manager, connecting to the WAGs through `establisher`.
164    ///
165    /// Returns an error if `config` does not [`UplinkManagerConfig::validate`].
166    pub fn new(
167        establisher: Establisher,
168        paths: PathManager,
169        config: UplinkManagerConfig,
170    ) -> anyhow::Result<Self> {
171        config.validate()?;
172
173        Ok(Self(Arc::new(UplinkManagerInner {
174            uplinks: RwLock::new(HashMap::new()),
175            establisher,
176            paths,
177            config,
178        })))
179    }
180}
181
182/// The error an uplink operation returns.
183///
184/// `status` holds the status code the WAG answered with, if the WAG answered at all. An operation
185/// that fails before the WAG answers, e.g. on a network error, carries no status.
186#[derive(Debug, thiserror::Error)]
187#[error("{source}")]
188pub struct UplinkError {
189    /// The status code the WAG answered with.
190    status: Option<StatusCode>,
191    #[source]
192    source: anyhow::Error,
193}
194
195impl UplinkError {
196    /// Creates an error for a WAG that refused the operation with `status`.
197    pub fn with_status(status: StatusCode, source: impl Into<anyhow::Error>) -> Self {
198        Self {
199            status: Some(status),
200            source: source.into(),
201        }
202    }
203
204    /// The status code the WAG answered with, if it answered.
205    pub fn status(&self) -> Option<StatusCode> {
206        self.status
207    }
208
209    /// Adds `context` to the error and keeps the status.
210    pub fn context(self, context: impl std::fmt::Display + Send + Sync + 'static) -> Self {
211        Self {
212            status: self.status,
213            source: self.source.context(context),
214        }
215    }
216}
217
218impl From<anyhow::Error> for UplinkError {
219    fn from(source: anyhow::Error) -> Self {
220        Self {
221            status: None,
222            source,
223        }
224    }
225}
226
227/// The error returned by [`UplinkManager::establish_stream`] when the client is not authorized
228#[derive(Debug, thiserror::Error)]
229pub enum EstablishStreamError {
230    /// The client holds no grant for the destination or for a segment of the path.
231    #[error("Client is not authorized to use the path")]
232    NotAuthorized,
233    /// The uplink could not be established, or the stream could not be opened on it.
234    #[error("Uplink error: {0}")]
235    UplinkError(UplinkError),
236}
237
238impl EstablishStreamError {
239    /// The status code the WAG answered with, if it answered.
240    pub fn status(&self) -> Option<StatusCode> {
241        match self {
242            Self::NotAuthorized => None,
243            Self::UplinkError(error) => error.status(),
244        }
245    }
246}
247
248impl<Establisher: UplinkEstablisher> UplinkManager<Establisher> {
249    /// Returns a reference to the establisher that opens the uplinks.
250    pub fn establisher(&self) -> &Establisher {
251        &self.0.establisher
252    }
253
254    /// Opens a stream for `client_ip` to `dst_sni` over the uplink for the given (path, WAG) pair,
255    /// establishing that uplink if it does not exist yet.
256    ///
257    /// The uplink stays alive as long as at least one of the returned guards is alive.
258    ///
259    /// The returned guard carries the client's authorization for the path, see
260    /// [`UplinkStreamGuard::grant_expired`]. `used_path` has to be the path
261    /// [`PathManager::best_path`] returned for this client and SNI.
262    pub async fn establish_stream(
263        &self,
264        client_ip: IpAddr,
265        sni: &WapSNI,
266        used_path: UsedPath,
267        wag: ScionSocketIpAddr,
268        now: SystemTime,
269    ) -> Result<UplinkStreamGuard<Establisher::Uplink>, EstablishStreamError> {
270        // Watch the client's grants before anything is established, so an unauthorized client
271        // cannot cause an uplink to be built.
272        let grants = self
273            .0
274            .paths
275            .watch_grants(client_ip, sni.customer_domain(), &used_path, now)
276            .map_err(|_| EstablishStreamError::NotAuthorized)?;
277
278        // Prepare the uplink and increase its stream count, so it is not cleaned while the stream
279        // is being established.
280        let reservation = self
281            .reserve_uplink(used_path, wag, now)
282            .await
283            .map_err(EstablishStreamError::UplinkError)?;
284
285        let stream = reservation
286            .entry()
287            .uplink
288            .establish_stream(sni)
289            .await
290            .map_err(EstablishStreamError::UplinkError)?;
291
292        Ok(reservation.attach(stream, grants))
293    }
294
295    /// Returns a reservation on the uplink for the given (path, WAG) pair, establishing it if
296    /// it does not exist yet.
297    ///
298    /// The reservation keeps the uplink from being reaped until it is dropped or turned into a
299    /// stream guard.
300    async fn reserve_uplink(
301        &self,
302        used_path: UsedPath,
303        wag: ScionSocketIpAddr,
304        now: SystemTime,
305    ) -> Result<UplinkReservation<Establisher::Uplink>, UplinkError> {
306        let key = UplinkKey {
307            path_fp: used_path.path.fingerprint(),
308            wag,
309        };
310
311        // Check if the uplink already exists. The reservation is taken while the map is
312        // locked, so the entry cannot be reaped between finding and reserving it.
313        //
314        // A closed uplink is not reused but replaced below
315        {
316            let uplinks = self.0.uplinks.read().unwrap();
317            if let Some(entry) = uplinks.get(&key).filter(|entry| !entry.is_closed()) {
318                return Ok(UplinkReservation::new(entry.clone()));
319            }
320        }
321
322        // Keep the segments the path was built from available for as long as the uplink lives, so
323        // it can still be re-pathed over them.
324        let segments_guard = self
325            .0
326            .paths
327            .hold_segments(&used_path, now)
328            .await
329            .context("Failed to guard the segments of the uplink path")?;
330
331        // Establish the connection without holding the lock. Two clients racing for the same
332        // key both establish one, and the loser's connection is dropped again below.
333        let closed = CancellationToken::new();
334        let uplink = self
335            .0
336            .establisher
337            .establish_connection(used_path.path.clone(), wag, closed.clone())
338            .await
339            .map_err(|e| e.context("Failed to establish uplink"))?;
340
341        let entry = Arc::new(UplinkEntry {
342            uplink,
343            established: now,
344            used_path: RwLock::new(used_path),
345            _segments_guard: segments_guard,
346            active_stream_count: AtomicUsize::new(0),
347            closed,
348        });
349
350        // Anything the map drops here still has our reservation counted on the new entry, so the
351        // replaced uplink is dropped once its own streams are gone.
352        let mut replaced = None;
353
354        let mut uplinks = self.0.uplinks.write().unwrap();
355        let entry = match uplinks.entry(key) {
356            std::collections::hash_map::Entry::Occupied(mut occupied)
357                if occupied.get().is_closed() =>
358            {
359                tracing::debug!(?key, "Replacing a closed uplink");
360                replaced = Some(occupied.insert(entry.clone()));
361
362                entry
363            }
364            std::collections::hash_map::Entry::Occupied(occupied) => {
365                tracing::debug!(?key, "Lost the race to establish an uplink, dropping ours");
366                occupied.get().clone()
367            }
368            std::collections::hash_map::Entry::Vacant(vacant) => {
369                tracing::debug!(?key, "Established a new uplink");
370                vacant.insert(entry).clone()
371            }
372        };
373
374        let reservation = UplinkReservation::new(entry);
375
376        drop(uplinks);
377        // Drop replaced outside the lock
378        drop(replaced);
379
380        Ok(reservation)
381    }
382
383    /// Runs the maintenance loop for the uplink manager.
384    ///
385    /// Reads the wall clock itself, unlike every other entry point. Never returns.
386    pub async fn run(&self) {
387        loop {
388            self.maintain(SystemTime::now()).await;
389            tokio::time::sleep(self.0.config.maintenance_interval).await;
390        }
391    }
392
393    /// Re-paths uplinks whose path is about to expire, then reaps the closed and unused ones.
394    pub async fn maintain(&self, now: SystemTime) {
395        // Snapshot the uplinks so the map is not locked while we compute paths.
396        let uplinks: Vec<(UplinkKey, Arc<UplinkEntry<Establisher::Uplink>>)> = self
397            .0
398            .uplinks
399            .read()
400            .unwrap()
401            .iter()
402            .map(|(key, entry)| (*key, entry.clone()))
403            .collect();
404
405        for (key, entry) in uplinks {
406            // A closed uplink is about to be dropped, re-pathing it would be wasted work.
407            if entry.is_closed() {
408                continue;
409            }
410
411            let used_path = entry.used_path();
412
413            // A path without an expiration, e.g. an AS local one, never needs refreshing.
414            let Some(expiry) = used_path.expiration() else {
415                continue;
416            };
417
418            if expiry > now + self.0.config.min_path_lifetime {
419                continue;
420            }
421
422            self.refresh_uplink_path(key, &entry, &used_path, now).await;
423        }
424
425        self.reap();
426    }
427
428    /// Drops the uplinks that are closed or that no stream is using anymore.
429    ///
430    /// A closed uplink goes regardless of its streams, since they cannot carry anything over it
431    /// either way. Everything the manager drops is closed first, so a stream still holding an entry
432    /// finds out that it is gone.
433    fn reap(&self) {
434        let mut reaped = Vec::new();
435
436        {
437            let mut uplinks = self.0.uplinks.write().unwrap();
438
439            uplinks.retain(|key, entry| {
440                let keep =
441                    !entry.is_closed() && entry.active_stream_count.load(Ordering::Relaxed) > 0;
442
443                if !keep {
444                    tracing::debug!(
445                        ?key,
446                        closed = entry.is_closed(),
447                        "Removing uplink from the manager"
448                    );
449
450                    entry.closed.cancel();
451                    reaped.push(entry.clone());
452                }
453
454                keep
455            });
456        }
457
458        // Dropped outside the lock: tearing a connection down may block.
459        drop(reaped);
460    }
461
462    /// Tries to give the uplink a path over the same segments with a later expiration.
463    async fn refresh_uplink_path(
464        &self,
465        key: UplinkKey,
466        entry: &UplinkEntry<Establisher::Uplink>,
467        used_path: &UsedPath,
468        now: SystemTime,
469    ) {
470        let refreshed = match self.0.paths.refresh_path(used_path, now).await {
471            Ok(refreshed) => refreshed,
472            Err(e) => {
473                tracing::warn!(
474                    ?key,
475                    ?e,
476                    "Failed to refresh path of uplink, keeping old version"
477                );
478                return;
479            }
480        };
481
482        if refreshed.expiration() <= used_path.expiration() {
483            tracing::debug!(
484                ?key,
485                "Refreshed path does not live longer, keeping the old one"
486            );
487            return;
488        }
489
490        // The refreshed path must stay reachable under the same key, otherwise a client
491        // asking for the old path would establish a second uplink over the new one.
492        if refreshed.path.fingerprint() != key.path_fp {
493            debug_assert!(
494                false,
495                "Refreshed path has a different fingerprint, this should not happen"
496            );
497            tracing::warn!(
498                ?key,
499                "Refreshed path has a different fingerprint, keeping the old one"
500            );
501            return;
502        }
503
504        if let Err(e) = entry.uplink.replace_path(refreshed.path.clone()) {
505            tracing::warn!(?key, "Uplink rejected the refreshed path: {e:#}");
506            return;
507        }
508
509        tracing::debug!(?key, expiry = ?refreshed.expiration(), "Refreshed the path of an uplink");
510
511        *entry.used_path.write().unwrap() = refreshed;
512    }
513}
514
515/// An established uplink and the state the manager keeps about it.
516pub struct UplinkEntry<UplinkType: GenericUplink> {
517    uplink: UplinkType,
518    established: SystemTime,
519    /// The path the uplink currently sends over, replaced when it is refreshed.
520    used_path: RwLock<UsedPath>,
521    /// Keeps the public segments of `used_path` available while the uplink lives.
522    ///
523    /// A refreshed path is built from the very same segments, so this stays valid across
524    /// [`UplinkManager::refresh_uplink_path`].
525    _segments_guard: PathSegmentsGuard,
526    active_stream_count: AtomicUsize,
527    /// Cancelled once the uplink is closed, either by the uplink itself after a network error or
528    /// by the manager when it drops the uplink.
529    closed: CancellationToken,
530}
531
532impl<UplinkType: GenericUplink> UplinkEntry<UplinkType> {
533    /// The path the uplink currently sends over.
534    pub fn used_path(&self) -> UsedPath {
535        self.used_path.read().unwrap().clone()
536    }
537
538    /// When the uplink was established.
539    pub fn established(&self) -> SystemTime {
540        self.established
541    }
542
543    /// Whether the uplink is closed, see [`UplinkEstablisher::establish_connection`].
544    pub fn is_closed(&self) -> bool {
545        self.closed.is_cancelled()
546    }
547
548    /// Resolves once the uplink is closed.
549    pub async fn closed(&self) {
550        self.closed.cancelled().await;
551    }
552
553    /// The uplink itself.
554    pub fn uplink(&self) -> &UplinkType {
555        &self.uplink
556    }
557
558    fn release_stream(&self) {
559        let previous = self.active_stream_count.fetch_sub(1, Ordering::Relaxed);
560        debug_assert!(previous > 0, "released a stream that was never reserved");
561    }
562}
563
564impl<UplinkType: GenericUplink> Drop for UplinkEntry<UplinkType> {
565    fn drop(&mut self) {
566        // Always cancel the uplink's closed token on drop
567        self.closed.cancel();
568    }
569}
570
571/// Keeps an uplink from being reaped while a stream is being established on it.
572struct UplinkReservation<UplinkType: GenericUplink>(Option<Arc<UplinkEntry<UplinkType>>>);
573
574impl<UplinkType: GenericUplink> UplinkReservation<UplinkType> {
575    /// Reserves the uplink. Must be called while the uplink map is locked, so the reservation
576    /// cannot race with [`UplinkManager::maintain`] reaping the entry.
577    fn new(entry: Arc<UplinkEntry<UplinkType>>) -> Self {
578        entry.active_stream_count.fetch_add(1, Ordering::Relaxed);
579        Self(Some(entry))
580    }
581
582    fn entry(&self) -> &Arc<UplinkEntry<UplinkType>> {
583        self.0
584            .as_ref()
585            .expect("the reservation is only taken by `attach`, which consumes self")
586    }
587
588    /// Turns the reservation into the guard of an established stream.
589    fn attach(
590        mut self,
591        stream: UplinkType::StreamType,
592        grants: GrantWatch,
593    ) -> UplinkStreamGuard<UplinkType> {
594        UplinkStreamGuard {
595            uplink_entry: self.0.take().expect("attach consumes self"),
596            stream: Some(stream),
597            grants,
598        }
599    }
600}
601
602impl<UplinkType: GenericUplink> Drop for UplinkReservation<UplinkType> {
603    fn drop(&mut self) {
604        // Still holding the reservation, i.e. no stream was established on it.
605        if let Some(entry) = self.0.take() {
606            entry.release_stream();
607        }
608    }
609}
610
611/// A stream on an uplink.
612///
613/// The uplink is kept alive for as long as this guard is.
614pub struct UplinkStreamGuard<UplinkType: GenericUplink> {
615    uplink_entry: Arc<UplinkEntry<UplinkType>>,
616    stream: Option<UplinkType::StreamType>,
617    /// The authorization of the client this stream was opened for.
618    grants: GrantWatch,
619}
620
621impl<UplinkType: GenericUplink> UplinkStreamGuard<UplinkType> {
622    /// Resolves once the client this stream belongs to loses its authorization for the path.
623    ///
624    /// The stream has to be torn down when this resolves.
625    ///
626    /// Cancel safe, so it can be awaited in a `select!`
627    pub async fn grant_expired(&self) {
628        self.grants.expired().await;
629    }
630
631    /// Resolves once the uplink underneath this stream is closed, so the stream has to be torn
632    /// down. The uplink may have failed, or the manager may simply be dropping it.
633    ///
634    /// This fires for every stream on the uplink, unlike [`Self::grant_expired`], which is per
635    /// client. Cancel safe.
636    pub async fn uplink_closed(&self) {
637        self.uplink_entry.closed().await;
638    }
639
640    /// Takes the stream out of the guard, so it can be used for forwarding.
641    pub fn take_stream(&mut self) -> Option<UplinkType::StreamType> {
642        self.stream.take()
643    }
644
645    /// The uplink the stream runs on.
646    pub fn uplink_entry(&self) -> &Arc<UplinkEntry<UplinkType>> {
647        &self.uplink_entry
648    }
649}
650
651impl<UplinkType: GenericUplink> Drop for UplinkStreamGuard<UplinkType> {
652    fn drop(&mut self) {
653        self.uplink_entry.release_stream();
654    }
655}
656
657/// Opens the connections towards the WAGs that the [`UplinkManager`] hands out streams on.
658///
659/// Separate from [`GenericUplink`] so that establishing can carry state of its own, e.g. the
660/// dataplane handle or socket the connections are made on.
661#[async_trait::async_trait]
662pub trait UplinkEstablisher: Send + Sync + 'static {
663    /// The uplinks this establishes.
664    type Uplink: GenericUplink;
665
666    /// Establishes a connection to `dst_addr` over `path`.
667    ///
668    /// `closed` ends the uplink's life and can be cancelled from both sides.
669    ///
670    /// The uplink has to cancel it once it is no longer usable, e.g. after a network error. An
671    /// uplink that never cancels the token keeps being used.
672    ///
673    /// The manager cancels it when it drops the uplink, which is not a failure: an uplink is also
674    /// dropped once nothing streams over it anymore. An implementation can await it to shut its own
675    /// tasks down.
676    ///
677    /// ### Parameters
678    /// - `path`: The SCION path to use for the uplink.
679    /// - `dst_addr`: The SCION socket address of the WAG to connect to.
680    /// - `closed`: A cancellation token that is cancelled when the uplink is should be closed. The
681    ///   implementation should also cancel it when the uplink is no longer usable.
682    // XXX: WAP <> WAG needs to authorize, but STREAM <-- WAG -->SERVER is using a different SNI.
683    // This is slightly akward, and should be looked at as part of the WAP<>WAG Authorization story
684    async fn establish_connection(
685        &self,
686        path: ScionPath,
687        dst_addr: ScionSocketIpAddr,
688        closed: CancellationToken,
689    ) -> Result<Self::Uplink, UplinkError>;
690}
691
692/// An established uplink to a WAG, as seen by the control plane.
693///
694/// The implementation lives in the dataplane; the control plane only decides which uplinks
695/// exist, which path they use, and when they are torn down.
696#[async_trait::async_trait]
697pub trait GenericUplink: Send + Sync + 'static {
698    /// A stream multiplexed onto the uplink.
699    type StreamType: Send;
700
701    /// Opens a new stream to `dst_sni` on this uplink.
702    ///
703    /// Sets [`UplinkError::status`] if the WAG refused the stream with a status code.
704    async fn establish_stream(&self, dst_sni: &WapSNI) -> Result<Self::StreamType, UplinkError>;
705
706    /// Switches the uplink over to `new_path` without interrupting its streams.
707    fn replace_path(&self, new_path: ScionPath) -> Result<(), UplinkError>;
708}
709
710#[cfg(test)]
711mod tests {
712    use std::sync::Mutex;
713
714    use futures::FutureExt;
715
716    use super::*;
717    use crate::pg_wap::{
718        paths::UsedPath,
719        test_util::{
720            Fixture, IDLE_EVICTION_TIME, MockFetcher, at, client_ip, core_ia, granted_id, leaf_ia,
721            other_client_ip, other_sni, sni, stranger_ip, up_segment, wag,
722        },
723    };
724
725    /// A `min_path_lifetime` longer than any test path lives, so every maintenance run tries to
726    /// re-path the uplinks.
727    const ALWAYS_REPATH: Duration = Duration::from_secs(30 * 24 * 3600);
728    /// A `min_path_lifetime` shorter than any test path lives, so maintenance never re-paths.
729    const NEVER_REPATH: Duration = Duration::from_secs(60);
730
731    /// Establishes [`TestUplink`]s and records what they were asked to do.
732    ///
733    /// A test hands one clone to its [`UplinkManager`] and keeps another to inspect the log and to
734    /// drive the failure paths; all clones share one state.
735    #[derive(Clone, Default)]
736    struct TestUplinks(Arc<Mutex<TestUplinkState>>);
737
738    /// What the [`TestUplink`]s of one test were asked to do, and how they should respond.
739    #[derive(Default)]
740    struct TestUplinkState {
741        /// Every operation the manager performed, in order.
742        log: Vec<UplinkEvent>,
743        /// The close tokens handed to the uplinks, in the order they were established, so a test
744        /// can close one from the dataplane side.
745        close_tokens: Vec<CancellationToken>,
746        /// Operations the test wants to fail, so the manager's error paths can be driven.
747        failing: Failures,
748    }
749
750    #[derive(Default)]
751    struct Failures {
752        connect: bool,
753        stream: bool,
754        /// The status the refused streams report, if the test set one.
755        stream_status: Option<StatusCode>,
756        replace_path: bool,
757    }
758
759    #[derive(Debug, PartialEq, Eq)]
760    enum UplinkEvent {
761        Connected(ScionSocketIpAddr),
762        ConnectionRefused,
763        Stream(String),
764        StreamRefused,
765        PathReplaced,
766        PathReplacementRejected,
767    }
768
769    impl TestUplinks {
770        fn with_state<T>(&self, f: impl FnOnce(&mut TestUplinkState) -> T) -> T {
771            f(&mut self.0.lock().unwrap())
772        }
773
774        /// Makes the uplinks established from now on fail, or stops doing so.
775        fn fail_connections(&self, fail: bool) {
776            self.with_state(|state| state.failing.connect = fail);
777        }
778
779        /// Makes the streams opened from now on fail without a status, or stops doing so.
780        fn fail_streams(&self, fail: bool) {
781            self.with_state(|state| {
782                state.failing.stream = fail;
783                state.failing.stream_status = None;
784            });
785        }
786
787        /// Makes the streams opened from now on fail with `status`.
788        fn fail_streams_with_status(&self, status: StatusCode) {
789            self.with_state(|state| {
790                state.failing.stream = true;
791                state.failing.stream_status = Some(status);
792            });
793        }
794
795        /// Makes the uplinks reject the paths the manager hands them, or stops doing so.
796        fn fail_path_replacements(&self, fail: bool) {
797            self.with_state(|state| state.failing.replace_path = fail);
798        }
799
800        /// Makes the uplink established `index`th report itself as unusable.
801        fn close(&self, index: usize) {
802            self.with_state(|state| state.close_tokens[index].cancel());
803        }
804
805        /// Whether the uplink established `index`th has been closed, by either side.
806        fn is_closed(&self, index: usize) -> bool {
807            self.with_state(|state| state.close_tokens[index].is_cancelled())
808        }
809
810        fn count_events(&self, matching: impl Fn(&UplinkEvent) -> bool) -> usize {
811            self.with_state(|state| state.log.iter().filter(|event| matching(event)).count())
812        }
813
814        fn path_replacements(&self) -> usize {
815            self.count_events(|event| matches!(event, UplinkEvent::PathReplaced))
816        }
817
818        fn path_replacement_attempts(&self) -> usize {
819            self.count_events(|event| {
820                matches!(
821                    event,
822                    UplinkEvent::PathReplaced | UplinkEvent::PathReplacementRejected
823                )
824            })
825        }
826
827        fn connections(&self) -> usize {
828            self.count_events(|event| matches!(event, UplinkEvent::Connected(_)))
829        }
830
831        /// Connections the manager tried to establish, successful or not.
832        fn connection_attempts(&self) -> usize {
833            self.count_events(|event| {
834                matches!(
835                    event,
836                    UplinkEvent::Connected(_) | UplinkEvent::ConnectionRefused
837                )
838            })
839        }
840
841        /// Streams the manager tried to open, successful or not.
842        fn stream_attempts(&self) -> usize {
843            self.count_events(|event| {
844                matches!(event, UplinkEvent::Stream(_) | UplinkEvent::StreamRefused)
845            })
846        }
847
848        fn streamed_snis(&self) -> Vec<String> {
849            self.with_state(|state| {
850                state
851                    .log
852                    .iter()
853                    .filter_map(|event| {
854                        match event {
855                            UplinkEvent::Stream(sni) => Some(sni.clone()),
856                            _ => None,
857                        }
858                    })
859                    .collect()
860            })
861        }
862    }
863
864    #[async_trait::async_trait]
865    impl UplinkEstablisher for TestUplinks {
866        type Uplink = TestUplink;
867
868        async fn establish_connection(
869            &self,
870            _path: ScionPath,
871            dst_addr: ScionSocketIpAddr,
872            closed: CancellationToken,
873        ) -> Result<Self::Uplink, UplinkError> {
874            self.with_state(|state| {
875                if state.failing.connect {
876                    state.log.push(UplinkEvent::ConnectionRefused);
877                    return Err(anyhow::anyhow!("the test asked connecting to fail").into());
878                }
879
880                state.log.push(UplinkEvent::Connected(dst_addr));
881                state.close_tokens.push(closed);
882                Ok(TestUplink(self.clone()))
883            })
884        }
885    }
886
887    /// One uplink established by [`TestUplinks`], reporting into the same state.
888    struct TestUplink(TestUplinks);
889
890    #[async_trait::async_trait]
891    impl GenericUplink for TestUplink {
892        type StreamType = ();
893
894        async fn establish_stream(
895            &self,
896            dst_sni: &WapSNI,
897        ) -> Result<Self::StreamType, UplinkError> {
898            self.0.with_state(|state| {
899                if state.failing.stream {
900                    state.log.push(UplinkEvent::StreamRefused);
901                    let error = anyhow::anyhow!("the test asked streaming to fail");
902
903                    return Err(match state.failing.stream_status {
904                        Some(status) => UplinkError::with_status(status, error),
905                        None => error.into(),
906                    });
907                }
908
909                state
910                    .log
911                    .push(UplinkEvent::Stream(dst_sni.full_domain().to_string()));
912                Ok(())
913            })
914        }
915
916        fn replace_path(&self, _new_path: ScionPath) -> Result<(), UplinkError> {
917            self.0.with_state(|state| {
918                if state.failing.replace_path {
919                    state.log.push(UplinkEvent::PathReplacementRejected);
920                    return Err(anyhow::anyhow!("the test asked path replacement to fail").into());
921                }
922
923                state.log.push(UplinkEvent::PathReplaced);
924                Ok(())
925            })
926        }
927    }
928
929    /// Everything a test needs: the primitives, the uplinks the manager establishes, the manager,
930    /// and a path to establish them over.
931    type Harness = (Fixture, TestUplinks, UplinkManager<TestUplinks>, UsedPath);
932
933    /// An uplink manager on top of `fixture`, which re-paths uplinks with less than
934    /// `min_path_lifetime` left.
935    fn manager_for(
936        fixture: &Fixture,
937        uplinks: TestUplinks,
938        min_path_lifetime: Duration,
939    ) -> UplinkManager<TestUplinks> {
940        UplinkManager::new(
941            uplinks,
942            fixture.paths.clone(),
943            UplinkManagerConfig {
944                min_path_lifetime,
945                ..UplinkManagerConfig::default()
946            },
947        )
948        .expect("a valid UplinkManagerConfig")
949    }
950
951    /// An uplink manager and an AS local path to establish uplinks over, which is built from no
952    /// segments at all.
953    async fn uplink_fixture() -> Harness {
954        let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
955        fixture.grant_non_core(Vec::new(), at(0));
956
957        let uplinks = TestUplinks::default();
958        let manager = manager_for(&fixture, uplinks.clone(), NEVER_REPATH);
959        let used = fixture
960            .paths
961            .best_path(client_ip(), &sni(), leaf_ia(), leaf_ia(), at(0))
962            .await
963            .expect("path combination succeeds")
964            .expect("an AS local path always exists");
965
966        (fixture, uplinks, manager, used)
967    }
968
969    /// An uplink manager and a path over a public up segment, so the path depends on the segment
970    /// store rather than on a grant.
971    async fn public_path_fixture(min_path_lifetime: Duration) -> Harness {
972        let fixture = Fixture::new(
973            MockFetcher::with_up_segments(vec![up_segment(0)]),
974            Duration::from_secs(10 * 24 * 3600),
975        );
976        // Granting the target without any segment leaves the public segment as the only input.
977        fixture.grant_non_core(Vec::new(), at(0));
978
979        let used = fixture
980            .paths
981            .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
982            .await
983            .expect("path combination succeeds")
984            .expect("the public segment yields a path");
985
986        let uplinks = TestUplinks::default();
987        let manager = manager_for(&fixture, uplinks.clone(), min_path_lifetime);
988
989        (fixture, uplinks, manager, used)
990    }
991
992    /// An uplink manager and a path over a privately granted up segment, so the path depends on a
993    /// grant rather than on the segment store.
994    async fn granted_path_fixture(
995        min_path_lifetime: Duration,
996        grant_duration: Duration,
997    ) -> Harness {
998        let fixture = Fixture::new(MockFetcher::empty(), grant_duration);
999        fixture.grant_non_core(vec![up_segment(0)], at(0));
1000
1001        let used = fixture
1002            .paths
1003            .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
1004            .await
1005            .expect("path combination succeeds")
1006            .expect("the granted segment yields a path");
1007
1008        let uplinks = TestUplinks::default();
1009        let manager = manager_for(&fixture, uplinks.clone(), min_path_lifetime);
1010
1011        (fixture, uplinks, manager, used)
1012    }
1013
1014    #[tokio::test]
1015    async fn uplinks_are_shared_per_path_and_wag() {
1016        let (fixture, uplinks, manager, used) = uplink_fixture().await;
1017        fixture.grant_target(client_ip(), other_sni().customer_domain(), at(0));
1018
1019        let first = manager
1020            .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1021            .await
1022            .expect("the first stream is established");
1023        let second = manager
1024            .establish_stream(
1025                client_ip(),
1026                &other_sni(),
1027                used.clone(),
1028                wag(leaf_ia()),
1029                at(0),
1030            )
1031            .await
1032            .expect("the second stream is established");
1033
1034        assert_eq!(
1035            uplinks.connections(),
1036            1,
1037            "the same (path, WAG) pair must share one uplink"
1038        );
1039        assert_eq!(
1040            uplinks.streamed_snis(),
1041            vec![sni().to_string(), other_sni().to_string()],
1042            "both SNIs get their own stream on that uplink"
1043        );
1044        assert!(std::ptr::eq(
1045            Arc::as_ptr(first.uplink_entry()),
1046            Arc::as_ptr(second.uplink_entry())
1047        ));
1048
1049        // A different WAG over the same path is a different uplink.
1050        let other_wag = manager
1051            .establish_stream(client_ip(), &sni(), used, wag(core_ia()), at(0))
1052            .await
1053            .expect("the stream to the other WAG is established");
1054        assert_eq!(uplinks.connections(), 2);
1055        assert!(!std::ptr::eq(
1056            Arc::as_ptr(first.uplink_entry()),
1057            Arc::as_ptr(other_wag.uplink_entry())
1058        ));
1059    }
1060
1061    #[tokio::test]
1062    async fn uplinks_without_streams_are_reaped() {
1063        let (_fixture, uplinks, manager, used) = uplink_fixture().await;
1064
1065        let stream = manager
1066            .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1067            .await
1068            .expect("the stream is established");
1069
1070        // As long as a stream is alive, the uplink survives maintenance and is reused.
1071        manager.maintain(at(0)).await;
1072        let second = manager
1073            .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1074            .await
1075            .expect("the second stream is established");
1076        assert_eq!(uplinks.connections(), 1);
1077
1078        drop(stream);
1079        drop(second);
1080
1081        manager.maintain(at(0)).await;
1082        manager
1083            .establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
1084            .await
1085            .expect("a new stream is established");
1086        assert_eq!(
1087            uplinks.connections(),
1088            2,
1089            "the reaped uplink has to be established again"
1090        );
1091    }
1092
1093    /// Two clients granted the same private segment, both streaming over the uplink it yields.
1094    async fn shared_uplink_fixture() -> Harness {
1095        let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
1096        fixture.grant_non_core(vec![up_segment(0)], at(0));
1097        fixture.grant_non_core_to(other_client_ip(), vec![up_segment(0)], at(0));
1098
1099        let uplinks = TestUplinks::default();
1100        let manager = manager_for(&fixture, uplinks.clone(), ALWAYS_REPATH);
1101
1102        let used = fixture
1103            .paths
1104            .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
1105            .await
1106            .expect("path combination succeeds")
1107            .expect("the granted segment yields a path");
1108
1109        (fixture, uplinks, manager, used)
1110    }
1111
1112    #[tokio::test]
1113    async fn a_shared_uplink_outlives_the_grant_of_the_client_that_established_it() {
1114        let (fixture, uplinks, manager, used) = shared_uplink_fixture().await;
1115
1116        let first = manager
1117            .establish_stream(client_ip(), &sni(), used.clone(), wag(core_ia()), at(0))
1118            .await
1119            .expect("the first client's stream is established");
1120        let second = manager
1121            .establish_stream(
1122                other_client_ip(),
1123                &sni(),
1124                used.clone(),
1125                wag(core_ia()),
1126                at(0),
1127            )
1128            .await
1129            .expect("the second client's stream is established");
1130        assert_eq!(uplinks.connections(), 1, "both clients share one uplink");
1131
1132        // The first client's grant lapses; the second refreshes and keeps the segment granted.
1133        fixture.grant_non_core_to(other_client_ip(), vec![up_segment(600)], at(90));
1134        fixture.grant.clean(at(101));
1135
1136        // The segment is no longer granted to the first client, but is still granted to the second,
1137        // which is what keeps the path usable.
1138        assert!(
1139            fixture
1140                .grant
1141                .segment_grant_expiry(
1142                    client_ip(),
1143                    sni().customer_domain(),
1144                    &granted_id(&up_segment(0)),
1145                    at(101)
1146                )
1147                .is_none()
1148        );
1149        assert!(
1150            fixture
1151                .grant
1152                .segment(&granted_id(&up_segment(0)), at(101))
1153                .is_some(),
1154            "a segment stays available while any client is granted it"
1155        );
1156
1157        // Only the client that lost its grant is told to tear down. Expiry is latched, so a
1158        // single poll of each is enough to tell them apart.
1159        assert!(
1160            first.grant_expired().now_or_never().is_some(),
1161            "the client whose grant lapsed must observe it"
1162        );
1163        assert!(
1164            second.grant_expired().now_or_never().is_none(),
1165            "the client that refreshed must keep its stream"
1166        );
1167
1168        // The uplink was established by the first client, but its path is still granted to the
1169        // second, so re-pathing it must still work.
1170        manager.maintain(at(101)).await;
1171        assert_eq!(
1172            uplinks.path_replacements(),
1173            1,
1174            "the uplink is re-pathed off a grant it was not established with"
1175        );
1176        assert_eq!(
1177            second.uplink_entry().used_path().expiration(),
1178            used.expiration().map(|old| old + Duration::from_secs(600))
1179        );
1180        assert_eq!(
1181            uplinks.connections(),
1182            1,
1183            "re-pathing must not reconnect the uplink"
1184        );
1185    }
1186
1187    #[tokio::test]
1188    async fn a_stream_is_refused_without_a_grant_for_the_paths_segments() {
1189        let (fixture, uplinks, manager, used) = shared_uplink_fixture().await;
1190
1191        // A third client is granted the target, but not the private segment the path uses.
1192        fixture.grant_target(stranger_ip(), sni().customer_domain(), at(0));
1193
1194        assert!(
1195            manager
1196                .establish_stream(stranger_ip(), &sni(), used, wag(core_ia()), at(0))
1197                .await
1198                .is_err(),
1199            "a client without a grant on the path's segments must not get a stream on it"
1200        );
1201        assert_eq!(
1202            uplinks.connection_attempts(),
1203            0,
1204            "and no uplink is established for it"
1205        );
1206    }
1207
1208    #[tokio::test]
1209    async fn a_stream_is_refused_for_an_ip_without_any_authorization() {
1210        let (fixture, uplinks, manager, used) = uplink_fixture().await;
1211
1212        assert!(!fixture.grant.ip_is_authorized(stranger_ip(), at(0)));
1213        assert!(
1214            manager
1215                .establish_stream(stranger_ip(), &sni(), used, wag(leaf_ia()), at(0))
1216                .await
1217                .is_err(),
1218            "an unauthenticated client must not get a stream"
1219        );
1220        assert_eq!(
1221            uplinks.connection_attempts(),
1222            0,
1223            "and must not cause any dataplane work"
1224        );
1225    }
1226
1227    #[tokio::test]
1228    async fn a_stream_is_refused_once_the_clients_grant_has_expired() {
1229        let (fixture, uplinks, manager, used) = uplink_fixture().await;
1230
1231        // The path is AS local and built from no segments at all, so the destination grant is the
1232        // only thing that can authorize it.
1233        assert!(used.segments.is_empty());
1234
1235        // The grant lapsed but nothing has swept it yet: the check must not depend on
1236        // `GrantManager::clean` having run.
1237        assert!(
1238            manager
1239                .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(101))
1240                .await
1241                .is_err(),
1242            "a lapsed grant does not authorize a stream, swept or not"
1243        );
1244
1245        fixture.grant.clean(at(101));
1246        assert!(
1247            manager
1248                .establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(101))
1249                .await
1250                .is_err(),
1251            "and still does not once it has been swept"
1252        );
1253        assert_eq!(
1254            uplinks.connection_attempts(),
1255            0,
1256            "an unauthorized client must not cause any dataplane work"
1257        );
1258    }
1259
1260    #[tokio::test]
1261    async fn an_uplink_is_reused_by_a_stream_that_arrives_before_the_reaper() {
1262        let (_fixture, uplinks, manager, used) = uplink_fixture().await;
1263
1264        let stream = manager
1265            .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1266            .await
1267            .expect("the stream is established");
1268        let entry = stream.uplink_entry().clone();
1269
1270        // The uplink has no streams left and is reapable, but until maintenance runs it is still
1271        // there to be picked up again.
1272        drop(stream);
1273        let second = manager
1274            .establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
1275            .await
1276            .expect("the second stream is established");
1277
1278        assert_eq!(
1279            uplinks.connections(),
1280            1,
1281            "an idle uplink is reused rather than replaced"
1282        );
1283        assert!(std::ptr::eq(
1284            Arc::as_ptr(&entry),
1285            Arc::as_ptr(second.uplink_entry())
1286        ));
1287
1288        // And the reservation the second stream took keeps it from being reaped after all.
1289        manager.maintain(at(0)).await;
1290        assert!(
1291            !second.uplink_entry().is_closed(),
1292            "an uplink that was picked up again must survive the reaper"
1293        );
1294    }
1295
1296    #[tokio::test]
1297    async fn a_closed_uplink_tears_down_its_streams_and_is_not_reused() {
1298        let (_fixture, uplinks, manager, used) = uplink_fixture().await;
1299
1300        let stream = manager
1301            .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1302            .await
1303            .expect("the stream is established");
1304        assert_eq!(uplinks.connections(), 1);
1305
1306        // Nothing has gone wrong yet, so no stream is asked to tear down.
1307        assert!(stream.uplink_closed().now_or_never().is_none());
1308
1309        // The dataplane reports the uplink as unusable.
1310        uplinks.close(0);
1311
1312        assert!(
1313            stream.uplink_closed().now_or_never().is_some(),
1314            "every stream on a closed uplink has to be told to tear down"
1315        );
1316
1317        // A new stream must not be put on the closed uplink, even before maintenance runs and even
1318        // though the old stream still holds it.
1319        let replacement = manager
1320            .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1321            .await
1322            .expect("the stream is established on a new uplink");
1323        assert_eq!(uplinks.connections(), 2, "the closed uplink is replaced");
1324        assert!(!std::ptr::eq(
1325            Arc::as_ptr(stream.uplink_entry()),
1326            Arc::as_ptr(replacement.uplink_entry())
1327        ));
1328        assert!(
1329            !replacement.uplink_entry().is_closed(),
1330            "the replacement is usable"
1331        );
1332    }
1333
1334    #[tokio::test]
1335    async fn a_closed_uplink_is_reaped_even_though_it_still_has_streams() {
1336        let (_fixture, uplinks, manager, used) = uplink_fixture().await;
1337
1338        let stream = manager
1339            .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1340            .await
1341            .expect("the stream is established");
1342
1343        uplinks.close(0);
1344        manager.maintain(at(0)).await;
1345
1346        // The stream is still alive and holding the entry, but the manager has let it go.
1347        let replacement = manager
1348            .establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
1349            .await
1350            .expect("the stream is established");
1351        assert_eq!(
1352            uplinks.connections(),
1353            2,
1354            "the reaped uplink has to be established again"
1355        );
1356        assert!(!std::ptr::eq(
1357            Arc::as_ptr(stream.uplink_entry()),
1358            Arc::as_ptr(replacement.uplink_entry())
1359        ));
1360    }
1361
1362    #[tokio::test]
1363    async fn reaping_an_unused_uplink_tells_its_holders_it_is_gone() {
1364        let (_fixture, _uplinks, manager, used) = uplink_fixture().await;
1365
1366        let stream = manager
1367            .establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
1368            .await
1369            .expect("the stream is established");
1370        let entry = stream.uplink_entry().clone();
1371
1372        // Dropping the last stream makes the uplink reapable; whoever still holds the entry has to
1373        // learn that it is no longer managed.
1374        drop(stream);
1375        manager.maintain(at(0)).await;
1376
1377        assert!(
1378            entry.is_closed(),
1379            "a reaped uplink counts as unusable for anything still holding it"
1380        );
1381    }
1382
1383    #[tokio::test]
1384    async fn a_live_uplink_holds_the_segments_of_its_path() {
1385        let (fixture, _uplinks, manager, used) = public_path_fixture(NEVER_REPATH).await;
1386
1387        let stream = manager
1388            .establish_stream(client_ip(), &sni(), used.clone(), wag(core_ia()), at(0))
1389            .await
1390            .expect("the stream is established");
1391
1392        // Well past the fixture's 60s idle eviction time, with nothing looking the pair up in the
1393        // meantime: the uplink's guard alone has to keep it managed.
1394        let held_at = at(121);
1395        fixture.segments.maintain(held_at).await;
1396
1397        assert_eq!(
1398            fixture.fetcher.calls(),
1399            1,
1400            "the guarded pair was never evicted, so it never had to be fetched again"
1401        );
1402        assert!(
1403            fixture.paths.refresh_path(&used, held_at).await.is_ok(),
1404            "the segments of a live uplink must stay resolvable"
1405        );
1406
1407        // Reaping the uplink releases its guard, and the pair goes back to being evicted when idle.
1408        drop(stream);
1409        manager.maintain(held_at).await;
1410
1411        let idle_at = at(242);
1412        fixture.segments.maintain(idle_at).await;
1413        fixture
1414            .segments
1415            .segments(leaf_ia(), core_ia(), idle_at)
1416            .await
1417            .expect("segments are fetched again");
1418        assert_eq!(
1419            fixture.fetcher.calls(),
1420            2,
1421            "an unheld pair is evicted again once it goes idle"
1422        );
1423    }
1424
1425    #[tokio::test]
1426    async fn uplinks_are_re_pathed_before_their_path_expires() {
1427        // Every path is inside the refresh window, so maintenance always tries to re-path.
1428        let (fixture, uplinks, manager, used) =
1429            granted_path_fixture(ALWAYS_REPATH, Duration::from_secs(10 * 24 * 3600)).await;
1430        let expiry = used
1431            .expiration()
1432            .expect("a combined path has an expiration");
1433
1434        let stream = manager
1435            .establish_stream(client_ip(), &sni(), used, wag(core_ia()), at(0))
1436            .await
1437            .expect("the stream is established");
1438
1439        // Nothing fresher is available yet, so the uplink keeps the path it has.
1440        manager.maintain(at(0)).await;
1441        assert_eq!(uplinks.path_replacements(), 0);
1442
1443        // The client refreshes its grant, which carries a newer copy of the same segment.
1444        fixture.grant_non_core(vec![up_segment(600)], at(0));
1445        manager.maintain(at(0)).await;
1446
1447        assert_eq!(
1448            uplinks.path_replacements(),
1449            1,
1450            "the uplink is given the newer path"
1451        );
1452        assert_eq!(
1453            stream.uplink_entry().used_path().expiration(),
1454            Some(expiry + Duration::from_secs(600))
1455        );
1456        assert_eq!(
1457            uplinks.connections(),
1458            1,
1459            "re-pathing must not reconnect the uplink"
1460        );
1461    }
1462
1463    #[tokio::test]
1464    async fn an_uplink_that_rejects_a_refreshed_path_keeps_the_one_it_has() {
1465        let (fixture, uplinks, manager, used) =
1466            granted_path_fixture(ALWAYS_REPATH, Duration::from_secs(10 * 24 * 3600)).await;
1467        let expiry = used
1468            .expiration()
1469            .expect("a combined path has an expiration");
1470
1471        let stream = manager
1472            .establish_stream(client_ip(), &sni(), used, wag(core_ia()), at(0))
1473            .await
1474            .expect("the stream is established");
1475
1476        // A newer copy of the segment is available, but the uplink refuses to switch over.
1477        fixture.grant_non_core(vec![up_segment(600)], at(0));
1478        uplinks.fail_path_replacements(true);
1479        manager.maintain(at(0)).await;
1480
1481        assert_eq!(
1482            uplinks.path_replacement_attempts(),
1483            1,
1484            "the refresh was offered"
1485        );
1486        assert_eq!(uplinks.path_replacements(), 0, "and was rejected");
1487        assert_eq!(
1488            stream.uplink_entry().used_path().expiration(),
1489            Some(expiry),
1490            "the uplink must keep sending over the path it still has"
1491        );
1492        assert!(
1493            !stream.uplink_entry().is_closed(),
1494            "a rejected refresh is not fatal to the uplink"
1495        );
1496
1497        // The uplink stays due for a refresh, so the next run offers it again.
1498        uplinks.fail_path_replacements(false);
1499        manager.maintain(at(0)).await;
1500        assert_eq!(uplinks.path_replacements(), 1);
1501        assert_eq!(
1502            stream.uplink_entry().used_path().expiration(),
1503            Some(expiry + Duration::from_secs(600))
1504        );
1505    }
1506
1507    #[tokio::test]
1508    async fn an_uplink_whose_segments_are_gone_keeps_the_path_it_has() {
1509        let (fixture, uplinks, manager, used) =
1510            granted_path_fixture(ALWAYS_REPATH, Duration::from_secs(100)).await;
1511        let expiry = used
1512            .expiration()
1513            .expect("a combined path has an expiration");
1514
1515        let stream = manager
1516            .establish_stream(client_ip(), &sni(), used.clone(), wag(core_ia()), at(0))
1517            .await
1518            .expect("the stream is established");
1519
1520        // The only grant on the path's segment lapses, so the path cannot be rebuilt at all.
1521        fixture.grant.clean(at(101));
1522        assert!(fixture.paths.refresh_path(&used, at(101)).await.is_err());
1523
1524        manager.maintain(at(101)).await;
1525
1526        assert_eq!(
1527            uplinks.path_replacement_attempts(),
1528            0,
1529            "there is no path to offer the uplink"
1530        );
1531        assert_eq!(
1532            stream.uplink_entry().used_path().expiration(),
1533            Some(expiry),
1534            "a failed refresh leaves the uplink on its current path"
1535        );
1536        assert!(
1537            !stream.uplink_entry().is_closed(),
1538            "the uplink is only torn down once nothing streams over it"
1539        );
1540    }
1541
1542    #[tokio::test]
1543    async fn a_failed_stream_releases_its_reservation_on_the_uplink() {
1544        let (_fixture, uplinks, manager, used) = uplink_fixture().await;
1545
1546        uplinks.fail_streams(true);
1547        assert!(
1548            manager
1549                .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1550                .await
1551                .is_err(),
1552            "the client gets no stream if the uplink cannot open one"
1553        );
1554        assert_eq!(
1555            uplinks.connections(),
1556            1,
1557            "the uplink itself was established before the stream failed"
1558        );
1559        assert_eq!(uplinks.stream_attempts(), 1);
1560
1561        // The reservation the failed stream held has to be gone, otherwise the uplink counts as
1562        // in use forever and is never reaped.
1563        manager.maintain(at(0)).await;
1564        assert!(
1565            uplinks.is_closed(0),
1566            "an uplink whose only stream failed has to be reaped"
1567        );
1568
1569        uplinks.fail_streams(false);
1570        manager
1571            .establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
1572            .await
1573            .expect("the next stream is established");
1574        assert_eq!(
1575            uplinks.connections(),
1576            2,
1577            "the reaped uplink had to be established again"
1578        );
1579    }
1580
1581    #[tokio::test]
1582    async fn a_refused_stream_reports_the_status_of_the_wag() {
1583        let (_fixture, uplinks, manager, used) = uplink_fixture().await;
1584
1585        uplinks.fail_streams_with_status(StatusCode::FORBIDDEN);
1586        let error = manager
1587            .establish_stream(client_ip(), &sni(), used.clone(), wag(leaf_ia()), at(0))
1588            .await
1589            .err()
1590            .expect("the uplink refused the stream");
1591        assert_eq!(
1592            error.status(),
1593            Some(StatusCode::FORBIDDEN),
1594            "the status of the WAG reaches the caller"
1595        );
1596
1597        uplinks.fail_streams(true);
1598        let error = manager
1599            .establish_stream(client_ip(), &sni(), used, wag(leaf_ia()), at(0))
1600            .await
1601            .err()
1602            .expect("the uplink refused the stream");
1603        assert_eq!(
1604            error.status(),
1605            None,
1606            "a failure without an answer from the WAG carries no status"
1607        );
1608    }
1609
1610    #[tokio::test]
1611    async fn a_failed_uplink_releases_the_segments_of_its_path() {
1612        let (fixture, uplinks, manager, used) = public_path_fixture(NEVER_REPATH).await;
1613
1614        uplinks.fail_connections(true);
1615        assert!(
1616            manager
1617                .establish_stream(client_ip(), &sni(), used.clone(), wag(core_ia()), at(0))
1618                .await
1619                .is_err(),
1620            "the client gets no stream if the uplink cannot be established"
1621        );
1622        assert_eq!(
1623            uplinks.connection_attempts(),
1624            1,
1625            "the connection was attempted"
1626        );
1627
1628        // The guard on the path's public segments went with the failed attempt, so the pair is
1629        // subject to idle eviction again.
1630        let idle_at = at(IDLE_EVICTION_TIME.as_secs() * 2 + 1);
1631        fixture.segments.maintain(idle_at).await;
1632        fixture
1633            .segments
1634            .segments(leaf_ia(), core_ia(), idle_at)
1635            .await
1636            .expect("segments are fetched again");
1637        assert_eq!(
1638            fixture.fetcher.calls(),
1639            2,
1640            "the pair of a failed uplink must not stay held"
1641        );
1642
1643        // Nothing was left in the manager either, so a later client establishes its own uplink.
1644        uplinks.fail_connections(false);
1645        manager
1646            .establish_stream(client_ip(), &sni(), used, wag(core_ia()), idle_at)
1647            .await
1648            .expect("the next stream is established");
1649        assert_eq!(uplinks.connections(), 1);
1650    }
1651
1652    #[tokio::test]
1653    async fn a_stream_is_refused_when_the_paths_segments_cannot_be_held() {
1654        let (fixture, uplinks, manager, used) = public_path_fixture(NEVER_REPATH).await;
1655
1656        // The pair backing the path goes idle and is evicted, and refetching it fails.
1657        let evicted_at = at(IDLE_EVICTION_TIME.as_secs() + 1);
1658        fixture.segments.maintain(evicted_at).await;
1659        fixture.fetcher.set_failing(true);
1660
1661        assert!(
1662            manager
1663                .establish_stream(client_ip(), &sni(), used, wag(core_ia()), evicted_at)
1664                .await
1665                .is_err(),
1666            "an uplink whose segments cannot be kept available must not be established"
1667        );
1668        assert_eq!(
1669            uplinks.connection_attempts(),
1670            0,
1671            "the segments are secured before anything is connected"
1672        );
1673    }
1674}