moqtap_proxy/listener.rs
1//! Unified listener — one UDP endpoint that accepts both raw-QUIC MoQT
2//! and WebTransport clients, dispatching per connection based on the
3//! ALPN the client negotiated during the TLS handshake.
4
5use std::net::SocketAddr;
6use std::sync::Arc;
7
8use moqtap_codec::version::DraftVersion;
9use rustls::pki_types::{CertificateDer, PrivateKeyDer};
10
11use crate::error::ProxyError;
12use crate::transport::{self, TransportInstaller, TransportProfile};
13use crate::types::Leg;
14
15/// WebTransport ALPN identifier.
16const H3_ALPN: &[u8] = b"h3";
17
18/// Configuration for the proxy's listener.
19pub struct ListenerConfig {
20 /// Address to bind to (e.g., `"0.0.0.0:4443"`).
21 pub bind_addr: SocketAddr,
22 /// TLS certificate chain (DER-encoded).
23 pub cert_chain: Vec<CertificateDer<'static>>,
24 /// TLS private key (DER-encoded).
25 pub key_der: PrivateKeyDer<'static>,
26 /// Optional QUIC transport parameters — flow-control windows, MTU,
27 /// keep-alive, congestion control — applied to every client
28 /// connection this listener accepts.
29 ///
30 /// `None` leaves quinn's defaults in place, which is the behaviour
31 /// callers had before this field existed.
32 ///
33 /// Setting this **and** [`ListenerConfig::transport_profile`] is
34 /// refused by [`Listener::bind`] rather than merged, with
35 /// [`ProxyError::TransportConfigAndProfile`] naming
36 /// [`Leg::Client`] — see that variant for why no merge is possible.
37 pub transport_config: Option<Arc<quinn::TransportConfig>>,
38 /// The same parameters as [`ListenerConfig::transport_config`], as a
39 /// value that can be written down, checked and stored.
40 ///
41 /// `Some(_)` builds the client leg's `quinn::TransportConfig` from this
42 /// profile — through [`ListenerConfig::installer`], or through
43 /// [`transport::DefaultInstaller`] when there is none — and installs it
44 /// before the endpoint exists. A profile the installer refuses is
45 /// [`ProxyError::TransportProfile`], and nothing is bound.
46 ///
47 /// `None` is the behaviour callers had before this field existed. It is
48 /// the *only* alternative to `transport_config`, never a companion to
49 /// it: a leg naming both is refused at bind time.
50 pub transport_profile: Option<TransportProfile>,
51 /// How [`ListenerConfig::transport_profile`] becomes the config this
52 /// leg installs.
53 ///
54 /// `None` uses [`transport::DefaultInstaller`], which applies the
55 /// profile over a fresh `quinn::TransportConfig::default()`. Supply one
56 /// to start from a base of your own instead — the trait exists because
57 /// a `quinn::TransportConfig` cannot be cloned, so the only way to have
58 /// a base *and* a profile is to build the base again for each leg.
59 ///
60 /// **Inert without a profile.** [`TransportInstaller::build`] takes a
61 /// profile, so an installer set beside an empty
62 /// [`ListenerConfig::transport_profile`] is never called and the leg
63 /// installs nothing. It is said here because a setting that is quietly
64 /// ignored is the failure this crate is least willing to hide.
65 ///
66 /// **It composes with a `qlog` spec** — named in plain code font
67 /// because that field exists only under the `qlog` feature, so a link
68 /// from this always-compiled one would not resolve. A leg carrying a
69 /// profile, a spec and an installer builds its config here, once, and
70 /// the capture sink is attached to what came back;
71 /// [`TransportInstaller::build`] returns an owned
72 /// `quinn::TransportConfig` precisely so that the two can stack.
73 pub installer: Option<Arc<dyn TransportInstaller>>,
74 /// Where this leg's QUIC-level capture is written, if it is captured at
75 /// all.
76 ///
77 /// `Some(_)` builds the client leg's `quinn::TransportConfig`, installs
78 /// the sink built from this spec on it, and hands that to the endpoint
79 /// — all before the endpoint exists, because quinn accepts a sink in
80 /// exactly one place and that place is a method which mutates a
81 /// `quinn::TransportConfig`. It composes with
82 /// [`ListenerConfig::transport_profile`], which is applied to the same
83 /// config first, and **not** with
84 /// [`ListenerConfig::transport_config`]: a leg naming a raw config and
85 /// a spec is refused at bind time with
86 /// [`ProxyError::TransportConfigAndQlog`] naming [`Leg::Client`], for
87 /// the reason written out on that variant.
88 ///
89 /// A spec on its own, with neither of the other two fields set, is
90 /// enough: the leg builds a `quinn::TransportConfig::default()` for the
91 /// sink to go on and installs it, rather than installing nothing and
92 /// leaving the capture attached to a config no connection uses.
93 ///
94 /// `None` is the behaviour callers had before this field existed, and
95 /// is how a leg says it does not want a capture. A spec that names no
96 /// writer is not that: it is refused with
97 /// [`ProxyError::Qlog`], because a spec is how a caller *asks* for a
98 /// capture.
99 ///
100 /// # Single-use, and therefore refused on a proxy template
101 ///
102 /// A [`QlogSpec`] owns its writer and is consumed when it becomes a
103 /// sink, so it has no `Clone`. [`TransparentProxy`] copies its
104 /// [`ListenerConfig`] template to build the listener it binds, and a
105 /// copy has nothing it could hand over — so a spec set on a
106 /// `ProxyConfig` could only be taken, counted as configured and
107 /// delivered nowhere. `TransparentProxy::run` therefore **refuses** such
108 /// a template with [`ProxyError::QlogOnProxyTemplate`], before it binds,
109 /// rather than dropping the field and coming up: a proxy that ran anyway
110 /// would report success and leave the caller's file uncreated, which
111 /// reads as a run that produced no events.
112 ///
113 /// Capture a client leg by building the [`ListenerConfig`] here and
114 /// calling [`Listener::bind`] yourself, which is also the only shape in
115 /// which one capture per connection is expressible: one sink shared by
116 /// an endpoint's connections writes all of them into one file, behind
117 /// one preamble, with no record saying where one ends.
118 ///
119 /// [`ProxyError::TransportConfigAndQlog`]: crate::error::ProxyError::TransportConfigAndQlog
120 /// [`ProxyError::Qlog`]: crate::error::ProxyError::Qlog
121 /// [`ProxyError::QlogOnProxyTemplate`]: crate::error::ProxyError::QlogOnProxyTemplate
122 /// [`QlogSpec`]: crate::qlog::QlogSpec
123 /// [`TransparentProxy`]: crate::proxy::TransparentProxy
124 #[cfg(feature = "qlog")]
125 pub qlog: Option<crate::qlog::QlogSpec>,
126}
127
128/// A client connection that has completed its handshake and is ready
129/// for MoQT session handling.
130///
131/// Produced by [`Listener::accept`]. Each variant corresponds to a
132/// distinct client-facing transport that MoQT can run over.
133pub enum AcceptedConn {
134 /// Raw QUIC connection speaking MoQT directly. The negotiated ALPN
135 /// (`moq-00`, `moqt-15`, `moqt-16`, `moqt-17`, …) is returned so
136 /// callers can resolve the draft version.
137 Quic {
138 /// The accepted QUIC connection.
139 conn: quinn::Connection,
140 /// The ALPN negotiated with the client.
141 alpn: Vec<u8>,
142 },
143 /// WebTransport session, with the H3 + extended-CONNECT dance
144 /// already completed by the listener.
145 #[cfg(feature = "webtransport")]
146 WebTransport(wtransport::Connection),
147}
148
149/// Build the ALPN list the server advertises to clients — every MoQT
150/// QUIC ALPN we support, plus `h3` when the WebTransport feature is on.
151///
152/// The list is derived from [`DraftVersion::quic_alpn`] so adding a new
153/// draft there automatically flows through to the proxy with no other
154/// changes required.
155fn advertised_alpns() -> Vec<Vec<u8>> {
156 // Dedup: drafts 07–14 all map to `moq-00`, so iterate every draft
157 // and keep unique ALPNs.
158 let mut out: Vec<Vec<u8>> = Vec::new();
159 for d in [
160 DraftVersion::Draft07,
161 DraftVersion::Draft08,
162 DraftVersion::Draft09,
163 DraftVersion::Draft10,
164 DraftVersion::Draft11,
165 DraftVersion::Draft12,
166 DraftVersion::Draft13,
167 DraftVersion::Draft14,
168 DraftVersion::Draft15,
169 DraftVersion::Draft16,
170 DraftVersion::Draft17,
171 DraftVersion::Draft18,
172 DraftVersion::Draft19,
173 ] {
174 let alpn = d.quic_alpn().to_vec();
175 if !out.iter().any(|existing| existing == &alpn) {
176 out.push(alpn);
177 }
178 }
179 #[cfg(feature = "webtransport")]
180 out.push(H3_ALPN.to_vec());
181 out
182}
183
184/// A transport-agnostic MoQT listener that accepts both raw-QUIC and
185/// WebTransport clients on the same UDP port.
186pub struct Listener {
187 endpoint: quinn::Endpoint,
188 /// The server configuration this endpoint was built with, kept so that
189 /// [`Listener::set_transport`] can replace one field of it without
190 /// rebuilding the rest.
191 ///
192 /// A clone of the value handed to quinn rather than a fresh build, and
193 /// the difference is not an optimisation. Rebuilding would re-parse the
194 /// certificate — which means retaining the private key here, and
195 /// `PrivateKeyDer` is not `Clone` — and `quinn::ServerConfig::with_crypto`
196 /// draws a fresh random handshake-token master key each time it is
197 /// called, which would invalidate every retry token already outstanding.
198 /// Keeping the built value costs one `Arc` per field and none of that.
199 server_config: quinn::ServerConfig,
200}
201
202impl Listener {
203 /// Bind to the configured address and start listening.
204 ///
205 /// The listener advertises every supported MoQT ALPN (`moq-00` and
206 /// `moqt-<N>` for all known drafts) plus `h3` for WebTransport. The
207 /// client picks which one to speak; the proxy forwards whatever
208 /// arrives.
209 ///
210 /// This binds an ordinary UDP socket at [`ListenerConfig::bind_addr`],
211 /// wraps it with quinn's default runtime adapter and hands it to
212 /// [`Listener::bind_with_socket`]. Must therefore be called from
213 /// inside a tokio runtime context — as it always had to be, because
214 /// quinn reaches for the same runtime when it binds a socket itself.
215 pub fn bind(config: ListenerConfig) -> Result<Self, ProxyError> {
216 let runtime = quinn::default_runtime()
217 .ok_or_else(|| ProxyError::Listener("no async runtime found".to_string()))?;
218 let socket = std::net::UdpSocket::bind(config.bind_addr)
219 .map_err(|e| ProxyError::Listener(e.to_string()))?;
220 let socket =
221 runtime.wrap_udp_socket(socket).map_err(|e| ProxyError::Listener(e.to_string()))?;
222
223 Self::bind_with_socket(config, socket)
224 }
225
226 /// Bind the listener over a caller-supplied abstract socket.
227 ///
228 /// Every datagram this listener sends to, or receives from, a client
229 /// passes through `socket`, so a caller that supplies a decorating
230 /// implementation — a tap, a counter, a network-impairment shim —
231 /// observes and can alter the whole client-facing leg. Ownership is
232 /// shared, so the caller keeps its handle on the socket after the
233 /// endpoint is running.
234 ///
235 /// [`ListenerConfig::bind_addr`] is ignored here: `socket` is already
236 /// bound, and its address is the one [`Listener::local_addr`] reports.
237 /// The rest of the configuration — the certificate, the advertised
238 /// ALPN list, the transport parameters — applies exactly as it does to
239 /// [`Listener::bind`], which is a thin wrapper around this function.
240 ///
241 /// # One socket covers WebTransport clients too
242 ///
243 /// This single seam reaches raw-QUIC and WebTransport clients alike,
244 /// because on the client-facing side the proxy never builds a
245 /// WebTransport endpoint of its own. It builds the QUIC endpoint here,
246 /// reads the negotiated ALPN off the handshake, and for `h3` clients
247 /// hands the still-connecting QUIC connection to the WebTransport
248 /// library to finish. The library adopts a connection that already
249 /// lives on this endpoint rather than binding a socket for it, so
250 /// there is no second datagram path to intercept.
251 ///
252 /// The relay leg to the upstream relay is a separate endpoint and is
253 /// not affected by this socket.
254 pub fn bind_with_socket(
255 config: ListenerConfig,
256 socket: Arc<dyn quinn::AsyncUdpSocket>,
257 ) -> Result<Self, ProxyError> {
258 // First, before the certificate is parsed and long before the
259 // endpoint is built. Every refusal this can produce is a fault in
260 // what the caller wrote rather than in the world, so a caller must
261 // not have to get a working certificate before hearing about one,
262 // and none of them must ever arrive attached to a live endpoint
263 // that then has to be torn down. It is also where a capture's sink
264 // is built, which writes the file's preamble — so a leg whose spec
265 // was refused has written nothing anywhere.
266 let transport = transport::resolve(
267 Leg::Client,
268 config.transport_config,
269 config.transport_profile.as_ref(),
270 config.installer.as_ref(),
271 // Moved out of the config rather than borrowed: a spec owns its
272 // writer and is consumed when it becomes a sink, so there is
273 // nothing here a second bind could use.
274 #[cfg(feature = "qlog")]
275 config.qlog,
276 )?;
277
278 let mut server_tls = rustls::ServerConfig::builder()
279 .with_no_client_auth()
280 .with_single_cert(config.cert_chain, config.key_der)
281 .map_err(|e| ProxyError::TlsConfig(format!("server cert config: {e}")))?;
282
283 server_tls.alpn_protocols = advertised_alpns();
284 server_tls.max_early_data_size = u32::MAX;
285
286 let quic_server_config: quinn::crypto::rustls::QuicServerConfig =
287 server_tls.try_into().map_err(|e| ProxyError::TlsConfig(format!("{e}")))?;
288
289 let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(quic_server_config));
290 if let Some(transport) = transport {
291 server_config.transport_config(transport);
292 }
293
294 let runtime = quinn::default_runtime()
295 .ok_or_else(|| ProxyError::Listener("no async runtime found".to_string()))?;
296
297 let endpoint = quinn::Endpoint::new_with_abstract_socket(
298 quinn::EndpointConfig::default(),
299 Some(server_config.clone()),
300 socket,
301 runtime,
302 )
303 .map_err(|e| ProxyError::Listener(e.to_string()))?;
304
305 Ok(Self { endpoint, server_config })
306 }
307
308 /// Install `transport` as the QUIC transport parameters this listener
309 /// gives to the connections it accepts **from now on**.
310 ///
311 /// # It cannot reach a connection that already exists
312 ///
313 /// A quinn connection takes its `TransportConfig` once, out of the
314 /// server configuration in force when its handshake began, and keeps
315 /// that `Arc` for as long as it lives. There is no way to hand a live
316 /// connection a different one — quinn exposes four setters on an
317 /// accepted connection (the two stream-count limits and the two
318 /// windows) and nothing else. So this changes what the *next* accepted
319 /// connection gets and leaves every connection already running exactly
320 /// as it was.
321 ///
322 /// That is worth stating rather than glossing, because the failure it
323 /// produces is silent: on a proxy nobody is connecting to any more,
324 /// this call succeeds, changes the endpoint, and never reaches a single
325 /// packet.
326 ///
327 /// Everything else about the endpoint — the certificate, the advertised
328 /// ALPN list, the handshake token key — is carried over from the
329 /// configuration the listener bound with, so a client's view of this
330 /// server is unchanged apart from the transport parameters.
331 pub(crate) fn set_transport(&self, transport: std::sync::Arc<quinn::TransportConfig>) {
332 let mut config = self.server_config.clone();
333 config.transport_config(transport);
334 self.endpoint.set_server_config(Some(config));
335 }
336
337 /// Accept the next incoming connection and dispatch based on the
338 /// ALPN negotiated during the TLS handshake.
339 ///
340 /// Raw-QUIC connections are returned immediately with the negotiated
341 /// ALPN so the caller can pick the MoQT draft. For `h3` clients the
342 /// listener drives the HTTP/3 + extended-CONNECT handshake to
343 /// completion before returning a ready `wtransport::Connection`.
344 pub async fn accept(&self) -> Result<AcceptedConn, ProxyError> {
345 let incoming = self
346 .endpoint
347 .accept()
348 .await
349 .ok_or_else(|| ProxyError::Listener("endpoint closed".to_string()))?;
350
351 let mut connecting = incoming.accept().map_err(|e| ProxyError::Listener(e.to_string()))?;
352
353 // Peeking at handshake_data resolves as soon as the server has
354 // processed the ClientHello, so the ALPN is known before the
355 // full handshake completes — and the Connecting is still live.
356 let hs_data = connecting
357 .handshake_data()
358 .await
359 .map_err(|e| ProxyError::Listener(format!("handshake data: {e}")))?;
360
361 let alpn = hs_data
362 .downcast::<quinn::crypto::rustls::HandshakeData>()
363 .ok()
364 .and_then(|hd| hd.protocol)
365 .map(|p| p.to_vec())
366 .unwrap_or_default();
367
368 if alpn == H3_ALPN {
369 #[cfg(feature = "webtransport")]
370 {
371 let session_fut =
372 wtransport::endpoint::IncomingSessionFuture::with_quic_connecting(connecting);
373 let session_request = session_fut
374 .await
375 .map_err(|e| ProxyError::Listener(format!("webtransport handshake: {e}")))?;
376 let conn = session_request
377 .accept()
378 .await
379 .map_err(|e| ProxyError::Listener(format!("webtransport accept: {e}")))?;
380 Ok(AcceptedConn::WebTransport(conn))
381 }
382 #[cfg(not(feature = "webtransport"))]
383 {
384 drop(connecting);
385 Err(ProxyError::Listener(
386 "client negotiated h3 but webtransport feature is not enabled".to_string(),
387 ))
388 }
389 } else {
390 let conn = connecting.await.map_err(|e| ProxyError::Listener(e.to_string()))?;
391 Ok(AcceptedConn::Quic { conn, alpn })
392 }
393 }
394
395 /// Get the local address this listener is bound to.
396 pub fn local_addr(&self) -> Result<SocketAddr, ProxyError> {
397 self.endpoint.local_addr().map_err(|e| ProxyError::Listener(e.to_string()))
398 }
399
400 /// Stop accepting new connections.
401 pub fn close(&self) {
402 self.endpoint.close(0u32.into(), b"proxy shutting down");
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use std::sync::atomic::{AtomicUsize, Ordering};
409
410 use rustls::pki_types::PrivatePkcs8KeyDer;
411
412 use super::*;
413 use crate::transport::TransportProfileError;
414
415 /// A certificate this listener will never get as far as parsing.
416 ///
417 /// Every refusal tested below has to be reported *before* the TLS
418 /// build, so the tests hand over ten bytes of nothing. If one of them
419 /// ever fails with a `TlsConfig` error, the check has drifted later
420 /// than the certificate and a caller now has to hold a valid identity
421 /// before they can be told their two transport fields contradict.
422 fn unusable_identity() -> (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>) {
423 (
424 vec![CertificateDer::from(vec![0u8; 10])],
425 PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(vec![0u8; 10])),
426 )
427 }
428
429 /// A real self-signed `localhost` pair, for the one test that has to
430 /// bind successfully.
431 fn usable_identity() -> (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>) {
432 let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
433 .expect("a key pair for a test certificate");
434 let params =
435 rcgen::CertificateParams::new(vec!["localhost".into()]).expect("certificate params");
436 let cert = params.self_signed(&key_pair).expect("self-sign");
437 (
438 vec![CertificateDer::from(cert.der().to_vec())],
439 PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der())),
440 )
441 }
442
443 fn config(identity: (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)) -> ListenerConfig {
444 let (cert_chain, key_der) = identity;
445 ListenerConfig {
446 bind_addr: "127.0.0.1:0".parse().expect("a literal address"),
447 cert_chain,
448 key_der,
449 transport_config: None,
450 transport_profile: None,
451 installer: None,
452 #[cfg(feature = "qlog")]
453 qlog: None,
454 }
455 }
456
457 /// Counts the builds and returns a config built the default way.
458 struct CountingInstaller(Arc<AtomicUsize>);
459
460 impl TransportInstaller for CountingInstaller {
461 fn build(
462 &self,
463 profile: &TransportProfile,
464 ) -> Result<quinn::TransportConfig, TransportProfileError> {
465 self.0.fetch_add(1, Ordering::Relaxed);
466 profile.into_config()
467 }
468 }
469
470 #[tokio::test]
471 async fn a_client_leg_naming_both_a_config_and_a_profile_is_refused_at_bind() {
472 let mut config = config(unusable_identity());
473 config.transport_config = Some(Arc::new(quinn::TransportConfig::default()));
474 config.transport_profile = Some(TransportProfile::default());
475
476 let err = Listener::bind(config).err().expect("a contradiction is not a listener");
477 assert!(
478 matches!(err, ProxyError::TransportConfigAndProfile { leg: Leg::Client }),
479 "the client leg's contradiction has to be reported as the client leg's: {err}"
480 );
481 }
482
483 /// The client leg's other contradiction, refused as its own thing and
484 /// with nothing written anywhere.
485 ///
486 /// Two halves. The first is that the refusal is
487 /// `TransportConfigAndQlog` and not `TransportConfigAndProfile`: the
488 /// two pairs have different fixes, and a caller told the wrong one goes
489 /// looking at the wrong half of their configuration. The second is what
490 /// makes this more than a claim about a return value — the sink writes
491 /// its preamble the instant it is built, so a writer that is still
492 /// empty afterwards is proof that no sink was built and no capture was
493 /// quietly begun on a leg that then refused to bind.
494 #[cfg(feature = "qlog")]
495 #[tokio::test]
496 async fn a_client_leg_naming_both_a_config_and_a_spec_is_refused_as_that() {
497 /// Everything written to it, readable while the writer is alive.
498 #[derive(Clone)]
499 struct Captured(Arc<std::sync::Mutex<Vec<u8>>>);
500
501 impl std::io::Write for Captured {
502 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
503 self.0.lock().expect("no test holds this across a panic").extend_from_slice(buf);
504 Ok(buf.len())
505 }
506
507 fn flush(&mut self) -> std::io::Result<()> {
508 Ok(())
509 }
510 }
511
512 let sink = Arc::new(std::sync::Mutex::new(Vec::new()));
513 let mut config = config(unusable_identity());
514 config.transport_config = Some(Arc::new(quinn::TransportConfig::default()));
515 config.qlog = Some(crate::qlog::QlogSpec {
516 writer: Some(Box::new(Captured(Arc::clone(&sink)))),
517 title: Some("client leg".to_string()),
518 description: None,
519 });
520
521 let err = Listener::bind(config).err().expect("a contradiction is not a listener");
522 assert!(
523 matches!(err, ProxyError::TransportConfigAndQlog { leg: Leg::Client }),
524 "a raw config and a spec is a different fault from a raw config and a profile, with a \
525 different fix, and one refusal covering both would send the caller to the wrong half \
526 of their configuration: {err}"
527 );
528 assert!(
529 sink.lock().expect("uncontended").is_empty(),
530 "the preamble is written the moment a sink is built, so anything here means the \
531 refused leg began a capture on its way to refusing"
532 );
533 }
534
535 #[tokio::test]
536 async fn a_client_leg_whose_profile_cannot_be_honoured_does_not_bind() {
537 let mut config = config(unusable_identity());
538 // quinn raises anything under 1200 to 1200 without a word, so a
539 // listener that came up here would be running at an MTU nobody
540 // asked for.
541 config.transport_profile =
542 Some(TransportProfile { initial_mtu: Some(900), ..Default::default() });
543
544 let err = Listener::bind(config).err().expect("an unhonourable profile is not a listener");
545 assert!(
546 matches!(
547 err,
548 ProxyError::TransportProfile {
549 leg: Leg::Client,
550 source: TransportProfileError::MtuBelowFloor { .. },
551 }
552 ),
553 "{err}"
554 );
555 }
556
557 #[tokio::test]
558 async fn a_profile_carrying_client_leg_installs_and_binds() {
559 let _ = rustls::crypto::ring::default_provider().install_default();
560
561 let builds = Arc::new(AtomicUsize::new(0));
562 let mut config = config(usable_identity());
563 config.transport_profile = Some(TransportProfile {
564 initial_mtu: Some(1350),
565 receive_window: Some(4 * 1024 * 1024),
566 ..Default::default()
567 });
568 config.installer = Some(Arc::new(CountingInstaller(Arc::clone(&builds))));
569
570 let listener = Listener::bind(config).expect("a valid profile binds a listener");
571 assert!(listener.local_addr().is_ok(), "the endpoint is live");
572 assert_eq!(
573 builds.load(Ordering::Relaxed),
574 1,
575 "the leg has to build its config through the installer, once, before the endpoint \
576 exists — a leg that bound without consulting it would be running on quinn's \
577 defaults and reporting success"
578 );
579 listener.close();
580 }
581}