1use std::net;
12use url::Url;
13
14use crate::RedactedUrl;
15
16const WIRE_VERSION: qmux::Version = qmux::Version::QMux01;
19
20#[derive(clap::Args, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
28#[group(id = "server-tcp")]
29#[serde(deny_unknown_fields, default)]
30#[non_exhaustive]
31pub struct Config {
32 #[arg(long = "server-tcp-bind", id = "server-tcp-bind", env = "MOQ_SERVER_TCP_BIND")]
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub bind: Option<net::SocketAddr>,
36}
37
38#[derive(Debug, thiserror::Error)]
40#[non_exhaustive]
41pub enum Error {
42 #[error(transparent)]
46 Io(#[from] std::io::Error),
47
48 #[error("missing hostname")]
50 MissingHostname,
51
52 #[error("missing port")]
54 MissingPort,
55
56 #[error("qmux connect failed")]
58 Connect(#[source] qmux::Error),
59
60 #[error("qmux accept failed")]
62 Accept(#[source] qmux::Error),
63
64 #[error("no addresses resolved")]
66 NoAddresses,
67
68 #[error("all {} connection attempts failed: {}", .0.len(), crate::failover::describe(.0))]
74 Failover(Vec<crate::failover::Failure<Error>>),
75}
76
77impl crate::failover::Aggregate for Error {
78 fn aggregate(failures: Vec<crate::failover::Failure<Self>>) -> Self {
79 Self::Failover(failures)
80 }
81
82 fn resolve(error: Option<std::io::Error>) -> Self {
83 match error {
84 Some(error) => Self::Io(error),
85 None => Self::NoAddresses,
86 }
87 }
88}
89
90type Result<T> = std::result::Result<T, Error>;
91
92pub(crate) async fn connect(
101 url: Url,
102 protocols: &[&str],
103 failover_delay: std::time::Duration,
104 resolution_delay: std::time::Duration,
105) -> Result<qmux::Session> {
106 let host = url.host().ok_or(Error::MissingHostname)?;
107 let port = url.port().ok_or(Error::MissingPort)?;
108
109 tracing::debug!(url = %RedactedUrl::new(&url), "connecting via TCP");
110 let candidates = crate::resolve::Candidates::resolve(host, port, resolution_delay);
111 connect_addrs(candidates, protocols, failover_delay).await
112}
113
114async fn connect_addrs(
117 candidates: crate::resolve::Candidates,
118 protocols: &[&str],
119 failover_delay: std::time::Duration,
120) -> Result<qmux::Session> {
121 crate::failover::race(candidates, failover_delay, |addr| {
122 let protocols: Vec<String> = protocols.iter().map(|&p| p.to_owned()).collect();
123 async move {
124 qmux::tcp::Config::new(WIRE_VERSION)
125 .protocols(protocols.iter().map(String::as_str))
126 .connect(addr)
127 .await
128 .map_err(Error::Connect)
129 }
130 })
131 .await
132}
133
134pub struct Listener {
136 listener: tokio::net::TcpListener,
137 protocols: Vec<String>,
138 health: crate::accept::Health,
139}
140
141impl Listener {
142 pub async fn bind(addr: net::SocketAddr) -> Result<Self> {
144 let listener = tokio::net::TcpListener::bind(addr).await?;
145 Ok(Self {
146 listener,
147 protocols: Vec::new(),
148 health: crate::accept::Health::new("tcp"),
149 })
150 }
151
152 pub fn accept_health(&self) -> crate::accept::Health {
155 self.health.clone()
156 }
157
158 pub fn with_accept_health(mut self, health: crate::accept::Health) -> Self {
164 self.health = health;
165 self
166 }
167
168 pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
171 where
172 I: IntoIterator<Item = S>,
173 S: Into<String>,
174 {
175 self.protocols = protocols.into_iter().map(Into::into).collect();
176 self
177 }
178
179 pub fn local_addr(&self) -> Result<net::SocketAddr> {
181 Ok(self.listener.local_addr()?)
182 }
183
184 pub async fn accept(&self) -> Option<Result<qmux::Session>> {
195 let (stream, addr) = self.accept_socket().await;
196 tracing::debug!(%addr, "accepted TCP connection");
197 let session = qmux::tcp::Config::new(WIRE_VERSION)
198 .protocols(self.protocols.iter().map(String::as_str))
199 .accept(stream)
200 .await
201 .map_err(Error::Accept);
202 Some(session)
203 }
204
205 async fn accept_socket(&self) -> (tokio::net::TcpStream, net::SocketAddr) {
207 loop {
208 match self.listener.accept().await {
209 Ok(accepted) => {
210 self.health.accepted();
211 return accepted;
212 }
213 Err(err) => {
214 if let Some(delay) = self.health.failed(&err) {
215 tokio::time::sleep(delay).await;
216 }
217 }
218 }
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use std::time::Duration;
227 use web_transport_trait::Session as _;
228
229 #[tokio::test]
233 async fn failover_recovers_from_blackhole_candidate() {
234 let listener = Listener::bind("127.0.0.1:0".parse().unwrap())
235 .await
236 .expect("bind listener")
237 .with_protocols(["moq-test"]);
238 let addr = listener.local_addr().expect("local addr");
239
240 let accept = tokio::spawn(async move { listener.accept().await.expect("listener gone").expect("accept") });
241
242 let blackhole: net::SocketAddr = "192.0.2.1:9".parse().unwrap();
243 let candidates = crate::resolve::Candidates::fixed([blackhole, addr]);
244 let session = tokio::time::timeout(
245 Duration::from_secs(5),
246 connect_addrs(candidates, &["moq-test"], Duration::from_millis(50)),
247 )
248 .await
249 .expect("failover timed out")
250 .expect("connect failed");
251
252 assert_eq!(session.protocol(), Some("moq-test"));
253 accept.await.expect("accept task panicked");
254 }
255
256 #[tokio::test]
257 async fn connect_addrs_rejects_empty() {
258 let candidates = crate::resolve::Candidates::fixed([]);
259 let res = connect_addrs(candidates, &["moq-test"], Duration::ZERO).await;
260 assert!(matches!(res, Err(Error::NoAddresses)));
261 }
262}