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)]
44 Io(#[from] std::io::Error),
45
46 #[error("missing hostname")]
48 MissingHostname,
49
50 #[error("missing port")]
52 MissingPort,
53
54 #[error("qmux connect failed")]
56 Connect(#[source] qmux::Error),
57
58 #[error("qmux accept failed")]
60 Accept(#[source] qmux::Error),
61
62 #[error("no addresses resolved")]
64 NoAddresses,
65
66 #[error("all {} connection attempts failed: {}", .0.len(), crate::failover::describe(.0))]
72 Failover(Vec<crate::failover::Failure<Error>>),
73}
74
75impl crate::failover::Aggregate for Error {
76 fn aggregate(failures: Vec<crate::failover::Failure<Self>>) -> Self {
77 Self::Failover(failures)
78 }
79
80 fn resolve(error: Option<std::io::Error>) -> Self {
81 match error {
82 Some(error) => Self::Io(error),
83 None => Self::NoAddresses,
84 }
85 }
86}
87
88type Result<T> = std::result::Result<T, Error>;
89
90pub(crate) async fn connect(
99 url: Url,
100 protocols: &[&str],
101 failover_delay: std::time::Duration,
102 resolution_delay: std::time::Duration,
103) -> Result<qmux::Session> {
104 let host = url.host().ok_or(Error::MissingHostname)?;
105 let port = url.port().ok_or(Error::MissingPort)?;
106
107 tracing::debug!(%url, "connecting via TCP");
108 let candidates = crate::resolve::Candidates::resolve(host, port, resolution_delay);
109 connect_addrs(candidates, protocols, failover_delay).await
110}
111
112async fn connect_addrs(
115 candidates: crate::resolve::Candidates,
116 protocols: &[&str],
117 failover_delay: std::time::Duration,
118) -> Result<qmux::Session> {
119 crate::failover::race(candidates, failover_delay, |addr| {
120 let protocols: Vec<String> = protocols.iter().map(|&p| p.to_owned()).collect();
121 async move {
122 qmux::tcp::Config::new(WIRE_VERSION)
123 .protocols(protocols.iter().map(String::as_str))
124 .connect(addr)
125 .await
126 .map_err(Error::Connect)
127 }
128 })
129 .await
130}
131
132pub struct Listener {
134 listener: tokio::net::TcpListener,
135 protocols: Vec<String>,
136 health: crate::accept::Health,
137}
138
139impl Listener {
140 pub async fn bind(addr: net::SocketAddr) -> Result<Self> {
142 let listener = tokio::net::TcpListener::bind(addr).await?;
143 Ok(Self {
144 listener,
145 protocols: Vec::new(),
146 health: crate::accept::Health::new("tcp"),
147 })
148 }
149
150 pub fn accept_health(&self) -> crate::accept::Health {
153 self.health.clone()
154 }
155
156 pub fn with_accept_health(mut self, health: crate::accept::Health) -> Self {
162 self.health = health;
163 self
164 }
165
166 pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
169 where
170 I: IntoIterator<Item = S>,
171 S: Into<String>,
172 {
173 self.protocols = protocols.into_iter().map(Into::into).collect();
174 self
175 }
176
177 pub fn local_addr(&self) -> Result<net::SocketAddr> {
179 Ok(self.listener.local_addr()?)
180 }
181
182 pub async fn accept(&self) -> Option<Result<qmux::Session>> {
193 let (stream, addr) = self.accept_socket().await;
194 tracing::debug!(%addr, "accepted TCP connection");
195 let session = qmux::tcp::Config::new(WIRE_VERSION)
196 .protocols(self.protocols.iter().map(String::as_str))
197 .accept(stream)
198 .await
199 .map_err(Error::Accept);
200 Some(session)
201 }
202
203 async fn accept_socket(&self) -> (tokio::net::TcpStream, net::SocketAddr) {
205 loop {
206 match self.listener.accept().await {
207 Ok(accepted) => {
208 self.health.accepted();
209 return accepted;
210 }
211 Err(err) => {
212 if let Some(delay) = self.health.failed(&err) {
213 tokio::time::sleep(delay).await;
214 }
215 }
216 }
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use std::time::Duration;
225 use web_transport_trait::Session as _;
226
227 #[tokio::test]
231 async fn failover_recovers_from_blackhole_candidate() {
232 let listener = Listener::bind("127.0.0.1:0".parse().unwrap())
233 .await
234 .expect("bind listener")
235 .with_protocols(["moq-test"]);
236 let addr = listener.local_addr().expect("local addr");
237
238 let accept = tokio::spawn(async move { listener.accept().await.expect("listener gone").expect("accept") });
239
240 let blackhole: net::SocketAddr = "192.0.2.1:9".parse().unwrap();
241 let candidates = crate::resolve::Candidates::fixed([blackhole, addr]);
242 let session = tokio::time::timeout(
243 Duration::from_secs(5),
244 connect_addrs(candidates, &["moq-test"], Duration::from_millis(50)),
245 )
246 .await
247 .expect("failover timed out")
248 .expect("connect failed");
249
250 assert_eq!(session.protocol(), Some("moq-test"));
251 accept.await.expect("accept task panicked");
252 }
253
254 #[tokio::test]
255 async fn connect_addrs_rejects_empty() {
256 let candidates = crate::resolve::Candidates::fixed([]);
257 let res = connect_addrs(candidates, &["moq-test"], Duration::ZERO).await;
258 assert!(matches!(res, Err(Error::NoAddresses)));
259 }
260}