use std::{
sync::{
Arc, Mutex as StdMutex,
atomic::{AtomicBool, Ordering},
},
time::Instant,
};
use async_trait::async_trait;
use scion_quic::socket::GenericScionUdpSocket;
use scion_stack::{
ScionStack, ScionStackBuilder,
resolver::{ScionDnsResolver, txt::ScionTxtDnsResolver},
stack::ScionSocketBindError,
};
use sciparse::address::ip_addr::ScionIpAddr;
use crate::{
config::{Config, EndhostApi, SharedEndhostApiSource, SharedTokenSource},
error::Error,
origin::Origin,
pool::{OriginClient, OriginMap},
};
#[async_trait]
pub(crate) trait SocketBinder: Send + Sync {
async fn bind(&self) -> Result<Arc<dyn GenericScionUdpSocket>, ScionSocketBindError>;
}
#[async_trait]
impl SocketBinder for ScionStack {
async fn bind(&self) -> Result<Arc<dyn GenericScionUdpSocket>, ScionSocketBindError> {
let socket = ScionStack::bind(self, None).await?;
Ok(Arc::new(socket))
}
}
pub(crate) struct Network {
binder: Arc<dyn SocketBinder>,
resolver: Arc<dyn ScionDnsResolver>,
}
impl Network {
pub(crate) fn new(
binder: Arc<dyn SocketBinder>,
resolver: Arc<dyn ScionDnsResolver>,
) -> Network {
Network { binder, resolver }
}
pub(crate) async fn bind(
&self,
) -> Result<Arc<dyn GenericScionUdpSocket>, ScionSocketBindError> {
self.binder.bind().await
}
pub(crate) async fn resolve(&self, host: &str) -> Result<Vec<ScionIpAddr>, Error> {
self.resolver
.resolve(host)
.await
.map_err(|e| Error::from_resolve_error(host, e))
}
}
pub(crate) struct Epoch {
network: Arc<Network>,
origins: StdMutex<OriginMap>,
closed: AtomicBool,
}
impl Epoch {
pub(crate) async fn build(config: &Config) -> Result<Epoch, Error> {
let resolver: Arc<dyn ScionDnsResolver> = match &config.resolver {
Some(resolver) => resolver.clone(),
None => default_resolver(config)?,
};
let mut builder = match &config.endhost_api {
EndhostApi::Fixed(url) => ScionStackBuilder::new().with_endhost_api(url.clone()),
EndhostApi::Source(source) => {
ScionStackBuilder::new()
.with_endhost_api_discovery_source(SharedEndhostApiSource(source.clone()))
}
};
if let Some(source) = &config.auth_token_source {
builder = builder.with_auth_token_source(SharedTokenSource(source.clone()));
}
if let Some(underlay) = config.preferred_underlay {
builder = builder.with_preferred_underlay(underlay);
}
if let Some(customizer) = &config.stack_customizer {
builder = customizer(builder);
}
let stack = builder.build().await.map_err(|e| {
let retryable = e.is_transient();
Error::StackBuild {
retryable,
source: Box::new(e),
}
})?;
Ok(Epoch::new(Arc::new(stack), resolver, config))
}
pub(crate) fn new(
binder: Arc<dyn SocketBinder>,
resolver: Arc<dyn ScionDnsResolver>,
config: &Config,
) -> Epoch {
Epoch {
network: Arc::new(Network::new(binder, resolver)),
origins: StdMutex::new(OriginMap::new(
config.max_origins,
config.idle_connection_timeout,
)),
closed: AtomicBool::new(false),
}
}
pub(crate) fn origin_client(
&self,
origin: Origin,
now: Instant,
config: &Config,
) -> Result<Arc<OriginClient>, Error> {
let mut origins = self.origins.lock().expect("origin map lock poisoned");
if self.closed.load(Ordering::Acquire) {
return Err(Error::Closed);
}
Ok(origins.get_or_insert(origin, now, config, &self.network))
}
pub(crate) async fn shutdown(self: Arc<Self>) {
self.closed.store(true, Ordering::Release);
let clients = {
let mut origins = self.origins.lock().expect("origin map lock poisoned");
origins.drain()
};
futures::future::join_all(clients.iter().map(|client| client.close())).await;
}
}
fn default_resolver(config: &Config) -> Result<Arc<dyn ScionDnsResolver>, Error> {
let resolver = ScionTxtDnsResolver::new().map_err(|e| {
Error::StackBuild {
retryable: false,
source: Box::new(e),
}
})?;
Ok(Arc::new(
resolver.with_overrides(config.dns_overrides.iter().cloned()),
))
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use scion_stack::ea_source::{
EndhostApiSource, EndhostApiSourceError, models::EndhostApiGroup,
};
use super::*;
struct FailingSource {
transient: bool,
}
#[async_trait]
impl EndhostApiSource for FailingSource {
async fn endhost_apis(&self) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError> {
Err(EndhostApiSourceError::new(
"no endhost APIs",
self.transient,
))
}
}
#[tokio::test]
async fn the_endhost_api_source_reaches_the_stack() {
for transient in [true, false] {
let config = Config::from_endhost_api_source(FailingSource { transient });
let error = Epoch::build(&config).await.err().expect("the source fails");
assert!(
matches!(error, Error::StackBuild { retryable, .. } if retryable == transient),
"{error:?}"
);
}
}
#[tokio::test]
async fn dns_overrides_reach_the_default_resolver() {
let addr = ScionIpAddr::new(
"1-ff00:0:110".parse().unwrap(),
Ipv4Addr::new(10, 0, 0, 1).into(),
);
let config = Config::new("https://endhost-api.invalid".parse().unwrap())
.with_dns_override("Pinned.example", vec![addr]);
let resolver = default_resolver(&config).unwrap();
assert_eq!(
resolver.resolve("pinned.example").await.unwrap(),
vec![addr]
);
}
}