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)]
43 Io(#[from] std::io::Error),
44
45 #[error("missing hostname")]
47 MissingHostname,
48
49 #[error("missing port")]
51 MissingPort,
52
53 #[error("qmux connect failed")]
55 Connect(#[source] qmux::Error),
56
57 #[error("qmux accept failed")]
59 Accept(#[source] qmux::Error),
60
61 #[error("no addresses resolved")]
63 NoAddresses,
64
65 #[error("all {} connection attempts failed: {}", .0.len(), crate::failover::describe(.0))]
71 Failover(Vec<crate::failover::Failure<Error>>),
72}
73
74impl crate::failover::Aggregate for Error {
75 fn aggregate(failures: Vec<crate::failover::Failure<Self>>) -> Self {
76 Self::Failover(failures)
77 }
78}
79
80type Result<T> = std::result::Result<T, Error>;
81
82pub(crate) async fn connect(
90 url: Url,
91 protocols: &[&str],
92 failover_delay: std::time::Duration,
93) -> Result<qmux::Session> {
94 let host = url.host_str().ok_or(Error::MissingHostname)?;
95 let port = url.port().ok_or(Error::MissingPort)?;
96
97 tracing::debug!(%url, "connecting via TCP");
98 let addrs = tokio::net::lookup_host((host, port)).await?;
99 connect_addrs(crate::failover::interleave(addrs), protocols, failover_delay).await
100}
101
102async fn connect_addrs(
105 candidates: Vec<net::SocketAddr>,
106 protocols: &[&str],
107 failover_delay: std::time::Duration,
108) -> Result<qmux::Session> {
109 if candidates.is_empty() {
110 return Err(Error::NoAddresses);
111 }
112
113 crate::failover::race(candidates, failover_delay, |addr| {
114 let protocols: Vec<String> = protocols.iter().map(|&p| p.to_owned()).collect();
115 async move {
116 qmux::tcp::Config::new(WIRE_VERSION)
117 .protocols(protocols.iter().map(String::as_str))
118 .connect(addr)
119 .await
120 .map_err(Error::Connect)
121 }
122 })
123 .await
124}
125
126pub struct Listener {
128 listener: tokio::net::TcpListener,
129 protocols: Vec<String>,
130 health: crate::accept::Health,
131}
132
133impl Listener {
134 pub async fn bind(addr: net::SocketAddr) -> Result<Self> {
136 let listener = tokio::net::TcpListener::bind(addr).await?;
137 Ok(Self {
138 listener,
139 protocols: Vec::new(),
140 health: crate::accept::Health::new("tcp"),
141 })
142 }
143
144 pub fn accept_health(&self) -> crate::accept::Health {
147 self.health.clone()
148 }
149
150 pub fn with_accept_health(mut self, health: crate::accept::Health) -> Self {
156 self.health = health;
157 self
158 }
159
160 pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
163 where
164 I: IntoIterator<Item = S>,
165 S: Into<String>,
166 {
167 self.protocols = protocols.into_iter().map(Into::into).collect();
168 self
169 }
170
171 pub fn local_addr(&self) -> Result<net::SocketAddr> {
173 Ok(self.listener.local_addr()?)
174 }
175
176 pub async fn accept(&self) -> Option<Result<qmux::Session>> {
187 let (stream, addr) = self.accept_socket().await;
188 tracing::debug!(%addr, "accepted TCP connection");
189 let session = qmux::tcp::Config::new(WIRE_VERSION)
190 .protocols(self.protocols.iter().map(String::as_str))
191 .accept(stream)
192 .await
193 .map_err(Error::Accept);
194 Some(session)
195 }
196
197 async fn accept_socket(&self) -> (tokio::net::TcpStream, net::SocketAddr) {
199 loop {
200 match self.listener.accept().await {
201 Ok(accepted) => {
202 self.health.accepted();
203 return accepted;
204 }
205 Err(err) => {
206 if let Some(delay) = self.health.failed(&err) {
207 tokio::time::sleep(delay).await;
208 }
209 }
210 }
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use std::time::Duration;
219 use web_transport_trait::Session as _;
220
221 #[tokio::test]
225 async fn failover_recovers_from_blackhole_candidate() {
226 let listener = Listener::bind("127.0.0.1:0".parse().unwrap())
227 .await
228 .expect("bind listener")
229 .with_protocols(["moq-test"]);
230 let addr = listener.local_addr().expect("local addr");
231
232 let accept = tokio::spawn(async move { listener.accept().await.expect("listener gone").expect("accept") });
233
234 let blackhole: net::SocketAddr = "192.0.2.1:9".parse().unwrap();
235 let session = tokio::time::timeout(
236 Duration::from_secs(5),
237 connect_addrs(vec![blackhole, addr], &["moq-test"], Duration::from_millis(50)),
238 )
239 .await
240 .expect("failover timed out")
241 .expect("connect failed");
242
243 assert_eq!(session.protocol(), Some("moq-test"));
244 accept.await.expect("accept task panicked");
245 }
246
247 #[tokio::test]
248 async fn connect_addrs_rejects_empty() {
249 let res = connect_addrs(Vec::new(), &["moq-test"], Duration::ZERO).await;
250 assert!(matches!(res, Err(Error::NoAddresses)));
251 }
252}