use std::net;
use url::Url;
const WIRE_VERSION: qmux::Version = qmux::Version::QMux01;
#[derive(clap::Args, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[group(id = "server-tcp")]
#[serde(deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Config {
#[arg(long = "server-tcp-bind", id = "server-tcp-bind", env = "MOQ_SERVER_TCP_BIND")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bind: Option<net::SocketAddr>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("missing hostname")]
MissingHostname,
#[error("missing port")]
MissingPort,
#[error("qmux connect failed")]
Connect(#[source] qmux::Error),
#[error("qmux accept failed")]
Accept(#[source] qmux::Error),
#[error("no addresses resolved")]
NoAddresses,
#[error("all {} connection attempts failed: {}", .0.len(), crate::failover::describe(.0))]
Failover(Vec<crate::failover::Failure<Error>>),
}
impl crate::failover::Aggregate for Error {
fn aggregate(failures: Vec<crate::failover::Failure<Self>>) -> Self {
Self::Failover(failures)
}
}
type Result<T> = std::result::Result<T, Error>;
pub(crate) async fn connect(
url: Url,
protocols: &[&str],
failover_delay: std::time::Duration,
) -> Result<qmux::Session> {
let host = url.host_str().ok_or(Error::MissingHostname)?;
let port = url.port().ok_or(Error::MissingPort)?;
tracing::debug!(%url, "connecting via TCP");
let addrs = tokio::net::lookup_host((host, port)).await?;
connect_addrs(crate::failover::interleave(addrs), protocols, failover_delay).await
}
async fn connect_addrs(
candidates: Vec<net::SocketAddr>,
protocols: &[&str],
failover_delay: std::time::Duration,
) -> Result<qmux::Session> {
if candidates.is_empty() {
return Err(Error::NoAddresses);
}
crate::failover::race(candidates, failover_delay, |addr| {
let protocols: Vec<String> = protocols.iter().map(|&p| p.to_owned()).collect();
async move {
qmux::tcp::Config::new(WIRE_VERSION)
.protocols(protocols.iter().map(String::as_str))
.connect(addr)
.await
.map_err(Error::Connect)
}
})
.await
}
pub struct Listener {
listener: tokio::net::TcpListener,
protocols: Vec<String>,
}
impl Listener {
pub async fn bind(addr: net::SocketAddr) -> Result<Self> {
let listener = tokio::net::TcpListener::bind(addr).await?;
Ok(Self {
listener,
protocols: Vec::new(),
})
}
pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.protocols = protocols.into_iter().map(Into::into).collect();
self
}
pub fn local_addr(&self) -> Result<net::SocketAddr> {
Ok(self.listener.local_addr()?)
}
pub async fn accept(&self) -> Option<Result<qmux::Session>> {
match self.listener.accept().await {
Ok((stream, addr)) => {
tracing::debug!(%addr, "accepted TCP connection");
let session = qmux::tcp::Config::new(WIRE_VERSION)
.protocols(self.protocols.iter().map(String::as_str))
.accept(stream)
.await
.map_err(Error::Accept);
Some(session)
}
Err(e) => Some(Err(e.into())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use web_transport_trait::Session as _;
#[tokio::test]
async fn failover_recovers_from_blackhole_candidate() {
let listener = Listener::bind("127.0.0.1:0".parse().unwrap())
.await
.expect("bind listener")
.with_protocols(["moq-test"]);
let addr = listener.local_addr().expect("local addr");
let accept = tokio::spawn(async move { listener.accept().await.expect("listener gone").expect("accept") });
let blackhole: net::SocketAddr = "192.0.2.1:9".parse().unwrap();
let session = tokio::time::timeout(
Duration::from_secs(5),
connect_addrs(vec![blackhole, addr], &["moq-test"], Duration::from_millis(50)),
)
.await
.expect("failover timed out")
.expect("connect failed");
assert_eq!(session.protocol(), Some("moq-test"));
accept.await.expect("accept task panicked");
}
#[tokio::test]
async fn connect_addrs_rejects_empty() {
let res = connect_addrs(Vec::new(), &["moq-test"], Duration::ZERO).await;
assert!(matches!(res, Err(Error::NoAddresses)));
}
}