1use std::net;
12use url::Url;
13
14const WIRE_VERSION: qmux::Version = qmux::Version::QMux01;
17
18#[derive(clap::Args, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
26#[group(id = "server-tcp")]
27#[serde(deny_unknown_fields, default)]
28#[non_exhaustive]
29pub struct Config {
30 #[arg(long = "server-tcp-bind", id = "server-tcp-bind", env = "MOQ_SERVER_TCP_BIND")]
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub bind: Option<net::SocketAddr>,
34}
35
36#[derive(Debug, thiserror::Error)]
38#[non_exhaustive]
39pub enum Error {
40 #[error(transparent)]
42 Io(#[from] std::io::Error),
43
44 #[error("missing hostname")]
46 MissingHostname,
47
48 #[error("missing port")]
50 MissingPort,
51
52 #[error("qmux connect failed")]
54 Connect(#[source] qmux::Error),
55
56 #[error("qmux accept failed")]
58 Accept(#[source] qmux::Error),
59
60 #[error("no addresses resolved")]
62 NoAddresses,
63
64 #[error("all {} connection attempts failed: {}", .0.len(), crate::failover::describe(.0))]
70 Failover(Vec<crate::failover::Failure<Error>>),
71}
72
73impl crate::failover::Aggregate for Error {
74 fn aggregate(failures: Vec<crate::failover::Failure<Self>>) -> Self {
75 Self::Failover(failures)
76 }
77}
78
79type Result<T> = std::result::Result<T, Error>;
80
81pub(crate) async fn connect(
89 url: Url,
90 protocols: &[&str],
91 failover_delay: std::time::Duration,
92) -> Result<qmux::Session> {
93 let host = url.host_str().ok_or(Error::MissingHostname)?;
94 let port = url.port().ok_or(Error::MissingPort)?;
95
96 tracing::debug!(%url, "connecting via TCP");
97 let addrs = tokio::net::lookup_host((host, port)).await?;
98 connect_addrs(crate::failover::interleave(addrs), protocols, failover_delay).await
99}
100
101async fn connect_addrs(
104 candidates: Vec<net::SocketAddr>,
105 protocols: &[&str],
106 failover_delay: std::time::Duration,
107) -> Result<qmux::Session> {
108 if candidates.is_empty() {
109 return Err(Error::NoAddresses);
110 }
111
112 crate::failover::race(candidates, failover_delay, |addr| {
113 let protocols: Vec<String> = protocols.iter().map(|&p| p.to_owned()).collect();
114 async move {
115 qmux::tcp::Config::new(WIRE_VERSION)
116 .protocols(protocols.iter().map(String::as_str))
117 .connect(addr)
118 .await
119 .map_err(Error::Connect)
120 }
121 })
122 .await
123}
124
125pub struct Listener {
127 listener: tokio::net::TcpListener,
128 protocols: Vec<String>,
129}
130
131impl Listener {
132 pub async fn bind(addr: net::SocketAddr) -> Result<Self> {
134 let listener = tokio::net::TcpListener::bind(addr).await?;
135 Ok(Self {
136 listener,
137 protocols: Vec::new(),
138 })
139 }
140
141 pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
144 where
145 I: IntoIterator<Item = S>,
146 S: Into<String>,
147 {
148 self.protocols = protocols.into_iter().map(Into::into).collect();
149 self
150 }
151
152 pub fn local_addr(&self) -> Result<net::SocketAddr> {
154 Ok(self.listener.local_addr()?)
155 }
156
157 pub async fn accept(&self) -> Option<Result<qmux::Session>> {
162 match self.listener.accept().await {
163 Ok((stream, addr)) => {
164 tracing::debug!(%addr, "accepted TCP connection");
165 let session = qmux::tcp::Config::new(WIRE_VERSION)
166 .protocols(self.protocols.iter().map(String::as_str))
167 .accept(stream)
168 .await
169 .map_err(Error::Accept);
170 Some(session)
171 }
172 Err(e) => Some(Err(e.into())),
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use std::time::Duration;
181 use web_transport_trait::Session as _;
182
183 #[tokio::test]
187 async fn failover_recovers_from_blackhole_candidate() {
188 let listener = Listener::bind("127.0.0.1:0".parse().unwrap())
189 .await
190 .expect("bind listener")
191 .with_protocols(["moq-test"]);
192 let addr = listener.local_addr().expect("local addr");
193
194 let accept = tokio::spawn(async move { listener.accept().await.expect("listener gone").expect("accept") });
195
196 let blackhole: net::SocketAddr = "192.0.2.1:9".parse().unwrap();
197 let session = tokio::time::timeout(
198 Duration::from_secs(5),
199 connect_addrs(vec![blackhole, addr], &["moq-test"], Duration::from_millis(50)),
200 )
201 .await
202 .expect("failover timed out")
203 .expect("connect failed");
204
205 assert_eq!(session.protocol(), Some("moq-test"));
206 accept.await.expect("accept task panicked");
207 }
208
209 #[tokio::test]
210 async fn connect_addrs_rejects_empty() {
211 let res = connect_addrs(Vec::new(), &["moq-test"], Duration::ZERO).await;
212 assert!(matches!(res, Err(Error::NoAddresses)));
213 }
214}