mod priority_connect;
use std::{borrow::Cow, fmt, net, sync::Arc, time::Duration};
use endhost_api_client::client::CrpcEndhostApiClient;
use rand::seq::IndexedRandom;
pub use scion_sdk_reqwest_connect_rpc::client::CrpcClientError;
use scion_sdk_reqwest_connect_rpc::token_source::{TokenSource, static_token::StaticTokenSource};
use scion_sdk_utils::backoff::ExponentialBackoff;
use url::Url;
use x25519_dalek::StaticSecret;
use crate::{
ea_source::{
EndhostApiSource, EndhostApiSourceError, StaticEndhostApiDiscovery, StaticEndhostApis,
},
scionstack::ScionStack,
underlays::{
SnapSocketConfig, UnderlayStack,
discovery::PeriodicUnderlayDiscovery,
udp::{LocalIpResolver, TargetAddrLocalIpResolver},
},
};
const DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL: Duration = Duration::from_secs(600);
const DEFAULT_ENDHOST_API_DISCOVERY_MAX_GROUPS: usize = 5;
const DEFAULT_ENDHOST_API_DISCOVERY_APIS_PER_GROUP: usize = 2;
const DEFAULT_ENDHOST_API_DISCOVERY_PER_GROUP_DELAY: Duration = Duration::from_millis(500);
pub struct ScionStackBuilder {
endhost_api_token_source: Option<Arc<dyn TokenSource>>,
auth_token_source: Option<Arc<dyn TokenSource>>,
endhost_api_source: Arc<dyn EndhostApiSource>,
preferred_underlay: PreferredUnderlay,
endhost_api_discovery: EndhostApiDiscoveryConfig,
snap: SnapUnderlayConfig,
udp: UdpUnderlayConfig,
}
impl ScionStackBuilder {
pub fn new() -> Self {
Self {
endhost_api_token_source: None,
auth_token_source: None,
endhost_api_source: Arc::new(StaticEndhostApiDiscovery::global()),
preferred_underlay: PreferredUnderlay::Udp,
endhost_api_discovery: EndhostApiDiscoveryConfig::default(),
snap: SnapUnderlayConfig::default(),
udp: UdpUnderlayConfig::default(),
}
}
pub fn with_prefer_snap(mut self) -> Self {
self.preferred_underlay = PreferredUnderlay::Snap;
self
}
pub fn with_prefer_udp(mut self) -> Self {
self.preferred_underlay = PreferredUnderlay::Udp;
self
}
pub fn with_endhost_api(mut self, endhost_api_url: Url) -> Self {
let source = StaticEndhostApis::new().add_group(vec![endhost_api_url]);
self.endhost_api_source = Arc::new(source);
self
}
pub fn with_endhost_api_discovery_source(mut self, source: impl EndhostApiSource) -> Self {
self.endhost_api_source = Arc::new(source);
self
}
pub fn with_endhost_api_auth_token_source(mut self, source: impl TokenSource) -> Self {
self.endhost_api_token_source = Some(Arc::new(source));
self
}
pub fn with_endhost_api_auth_token(mut self, token: String) -> Self {
self.endhost_api_token_source = Some(Arc::new(StaticTokenSource::from(token)));
self
}
pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
self.auth_token_source = Some(Arc::new(source));
self
}
pub fn with_auth_token(mut self, token: String) -> Self {
self.auth_token_source = Some(Arc::new(StaticTokenSource::from(token)));
self
}
pub fn with_endhost_api_discovery_max_groups(mut self, max_groups: usize) -> Self {
self.endhost_api_discovery.max_groups = max_groups;
self
}
pub fn with_endhost_api_discovery_apis_per_group(mut self, apis_per_group: usize) -> Self {
self.endhost_api_discovery.apis_per_group = apis_per_group;
self
}
pub fn with_endhost_api_discovery_per_group_delay(mut self, per_group_delay: Duration) -> Self {
self.endhost_api_discovery.per_group_delay = per_group_delay;
self
}
pub fn with_snap_underlay_config(mut self, config: SnapUnderlayConfig) -> Self {
self.snap = config;
self
}
pub fn with_udp_underlay_config(mut self, config: UdpUnderlayConfig) -> Self {
self.udp = config;
self
}
pub async fn build(self) -> Result<ScionStack, BuildScionStackError> {
let ScionStackBuilder {
endhost_api_token_source,
auth_token_source,
endhost_api_source,
preferred_underlay,
endhost_api_discovery,
snap,
udp,
} = self;
let api_groups = endhost_api_source.endhost_apis().await?;
let api_groups: Vec<Vec<Url>> = {
let mut rng = rand::rng();
api_groups
.into_iter()
.map(|g| g.apis.into_iter().map(|a| a.address).collect::<Vec<_>>())
.filter(|group| !group.is_empty())
.take(endhost_api_discovery.max_groups)
.map(|group: Vec<Url>| {
group
.sample(&mut rng, endhost_api_discovery.apis_per_group)
.cloned()
.collect()
})
.collect()
};
if api_groups.is_empty() {
return Err(BuildScionStackError::EndhostApiSourceError(
EndhostApiSourceError {
error: anyhow::anyhow!("Endhost API discovery returned no APIs"),
transient: false,
},
));
}
let token_source: Option<Arc<dyn TokenSource>> =
endhost_api_token_source.or(auth_token_source.clone());
let discover_underlays = move |url: Url| {
let token_source = token_source.clone();
let url = url.clone();
async move {
let mut client =
CrpcEndhostApiClient::new(&url).map_err(ApiAttemptError::ClientSetup)?;
if let Some(token_source) = &token_source {
client.use_token_source(token_source.clone());
}
let client = Arc::new(client);
let discovery = PeriodicUnderlayDiscovery::new(
client.clone(),
udp.udp_next_hop_resolver_fetch_interval,
ExponentialBackoff::new(0.5, 10.0, 2.0, 0.5),
)
.await
.map_err(ApiAttemptError::UnderlayDiscovery)?;
Ok((client, discovery))
}
};
let (api_url, (endhost_api_client, underlay_discovery)) =
priority_connect::try_priority_groups(
api_groups,
discover_underlays,
endhost_api_discovery.per_group_delay,
)
.await
.map_err(|errors| {
BuildScionStackError::AllEndhostApisFailed(AllEndhostApisFailed(errors))
})?;
tracing::info!(%api_url, "Successfully selected endhost API");
let local_ip_resolver: Arc<dyn LocalIpResolver> = match udp.local_ips {
Some(ips) => Arc::new(ips),
None => {
Arc::new(
TargetAddrLocalIpResolver::new(api_url.clone())
.map_err(BuildUdpScionStackError::LocalIpResolutionError)?,
)
}
};
let underlay_stack = UnderlayStack::new(
preferred_underlay,
Arc::new(underlay_discovery),
local_ip_resolver,
snap.static_identity.unwrap_or_else(StaticSecret::random),
SnapSocketConfig {
snap_token_source: snap.snap_token_source.or(auth_token_source),
},
);
Ok(ScionStack::new(
Some(api_url),
endhost_api_client,
Arc::new(underlay_stack),
))
}
}
impl Default for ScionStackBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(thiserror::Error, Debug)]
pub enum BuildScionStackError {
#[error("no underlay available: {0}")]
UnderlayUnavailable(Cow<'static, str>),
#[error(transparent)]
AllEndhostApisFailed(#[from] AllEndhostApisFailed),
#[error("endhost api source error: {0:#}")]
EndhostApiSourceError(#[from] EndhostApiSourceError),
#[error(transparent)]
Snap(#[from] BuildSnapScionStackError),
#[error(transparent)]
Udp(#[from] BuildUdpScionStackError),
#[error("internal error: {0:#}")]
Internal(anyhow::Error),
}
#[derive(thiserror::Error, Debug)]
pub enum BuildSnapScionStackError {
#[error("no SNAP data plane available: {0}")]
DataPlaneUnavailable(Cow<'static, str>),
#[error("control plane client setup error: {0:#}")]
ControlPlaneClientSetupError(anyhow::Error),
#[error("data plane discovery request error: {0:#}")]
DataPlaneDiscoveryError(CrpcClientError),
}
#[derive(thiserror::Error, Debug)]
pub enum BuildUdpScionStackError {
#[error("local IP resolution error: {0:#}")]
LocalIpResolutionError(anyhow::Error),
}
#[derive(Debug)]
pub struct AllEndhostApisFailed(pub Vec<(Url, ApiAttemptError)>);
impl fmt::Display for AllEndhostApisFailed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "all {} endhost API(s) failed", self.0.len())?;
let mut sep = ": ";
for (url, err) in &self.0 {
write!(f, "{sep}{url} ({err})")?;
sep = "; ";
}
Ok(())
}
}
impl std::error::Error for AllEndhostApisFailed {}
#[derive(thiserror::Error, Debug)]
pub enum ApiAttemptError {
#[error("client setup: {0:#}")]
ClientSetup(anyhow::Error),
#[error("underlay discovery: {0:#}")]
UnderlayDiscovery(CrpcClientError),
}
pub struct EndhostApiDiscoveryConfig {
max_groups: usize,
apis_per_group: usize,
per_group_delay: Duration,
}
impl Default for EndhostApiDiscoveryConfig {
fn default() -> Self {
Self {
max_groups: DEFAULT_ENDHOST_API_DISCOVERY_MAX_GROUPS,
apis_per_group: DEFAULT_ENDHOST_API_DISCOVERY_APIS_PER_GROUP,
per_group_delay: DEFAULT_ENDHOST_API_DISCOVERY_PER_GROUP_DELAY,
}
}
}
pub enum PreferredUnderlay {
Snap,
Udp,
}
#[derive(Default)]
pub struct SnapUnderlayConfig {
snap_token_source: Option<Arc<dyn TokenSource>>,
snap_dp_index: usize,
static_identity: Option<StaticSecret>,
}
impl SnapUnderlayConfig {
pub fn builder() -> SnapUnderlayConfigBuilder {
SnapUnderlayConfigBuilder(Self::default())
}
}
pub struct SnapUnderlayConfigBuilder(SnapUnderlayConfig);
impl SnapUnderlayConfigBuilder {
pub fn with_auth_token(mut self, token: String) -> Self {
self.0.snap_token_source = Some(Arc::new(StaticTokenSource::from(token)));
self
}
pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
self.0.snap_token_source = Some(Arc::new(source));
self
}
pub fn with_snap_dp_index(mut self, dp_index: usize) -> Self {
self.0.snap_dp_index = dp_index;
self
}
pub fn with_static_identity(mut self, identity: StaticSecret) -> Self {
self.0.static_identity = Some(identity);
self
}
pub fn build(self) -> SnapUnderlayConfig {
self.0
}
}
pub struct UdpUnderlayConfig {
udp_next_hop_resolver_fetch_interval: Duration,
local_ips: Option<Vec<net::IpAddr>>,
}
impl Default for UdpUnderlayConfig {
fn default() -> Self {
Self {
udp_next_hop_resolver_fetch_interval: DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL,
local_ips: None,
}
}
}
impl UdpUnderlayConfig {
pub fn builder() -> UdpUnderlayConfigBuilder {
UdpUnderlayConfigBuilder(Self::default())
}
}
pub struct UdpUnderlayConfigBuilder(UdpUnderlayConfig);
impl UdpUnderlayConfigBuilder {
pub fn with_local_ips(mut self, local_ips: Vec<net::IpAddr>) -> Self {
self.0.local_ips = Some(local_ips);
self
}
pub fn with_udp_next_hop_resolver_fetch_interval(mut self, fetch_interval: Duration) -> Self {
self.0.udp_next_hop_resolver_fetch_interval = fetch_interval;
self
}
pub fn build(self) -> UdpUnderlayConfig {
self.0
}
}