Skip to main content

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;
12
13/// WebTransport ALPN identifier.
14const H3_ALPN: &[u8] = b"h3";
15
16/// Configuration for the proxy's listener.
17pub struct ListenerConfig {
18    /// Address to bind to (e.g., `"0.0.0.0:4443"`).
19    pub bind_addr: SocketAddr,
20    /// TLS certificate chain (DER-encoded).
21    pub cert_chain: Vec<CertificateDer<'static>>,
22    /// TLS private key (DER-encoded).
23    pub key_der: PrivateKeyDer<'static>,
24}
25
26/// A client connection that has completed its handshake and is ready
27/// for MoQT session handling.
28///
29/// Produced by [`Listener::accept`]. Each variant corresponds to a
30/// distinct client-facing transport that MoQT can run over.
31pub enum AcceptedConn {
32    /// Raw QUIC connection speaking MoQT directly. The negotiated ALPN
33    /// (`moq-00`, `moqt-15`, `moqt-16`, `moqt-17`, …) is returned so
34    /// callers can resolve the draft version.
35    Quic {
36        /// The accepted QUIC connection.
37        conn: quinn::Connection,
38        /// The ALPN negotiated with the client.
39        alpn: Vec<u8>,
40    },
41    /// WebTransport session, with the H3 + extended-CONNECT dance
42    /// already completed by the listener.
43    #[cfg(feature = "webtransport")]
44    WebTransport(wtransport::Connection),
45}
46
47/// Build the ALPN list the server advertises to clients — every MoQT
48/// QUIC ALPN we support, plus `h3` when the WebTransport feature is on.
49///
50/// The list is derived from [`DraftVersion::quic_alpn`] so adding a new
51/// draft there automatically flows through to the proxy with no other
52/// changes required.
53fn advertised_alpns() -> Vec<Vec<u8>> {
54    // Dedup: drafts 07–14 all map to `moq-00`, so iterate every draft
55    // and keep unique ALPNs.
56    let mut out: Vec<Vec<u8>> = Vec::new();
57    for d in [
58        DraftVersion::Draft07,
59        DraftVersion::Draft08,
60        DraftVersion::Draft09,
61        DraftVersion::Draft10,
62        DraftVersion::Draft11,
63        DraftVersion::Draft12,
64        DraftVersion::Draft13,
65        DraftVersion::Draft14,
66        DraftVersion::Draft15,
67        DraftVersion::Draft16,
68        DraftVersion::Draft17,
69        DraftVersion::Draft18,
70        DraftVersion::Draft19,
71    ] {
72        let alpn = d.quic_alpn().to_vec();
73        if !out.iter().any(|existing| existing == &alpn) {
74            out.push(alpn);
75        }
76    }
77    #[cfg(feature = "webtransport")]
78    out.push(H3_ALPN.to_vec());
79    out
80}
81
82/// A transport-agnostic MoQT listener that accepts both raw-QUIC and
83/// WebTransport clients on the same UDP port.
84pub struct Listener {
85    endpoint: quinn::Endpoint,
86}
87
88impl Listener {
89    /// Bind to the configured address and start listening.
90    ///
91    /// The listener advertises every supported MoQT ALPN (`moq-00` and
92    /// `moqt-<N>` for all known drafts) plus `h3` for WebTransport. The
93    /// client picks which one to speak; the proxy forwards whatever
94    /// arrives.
95    pub fn bind(config: ListenerConfig) -> Result<Self, ProxyError> {
96        let mut server_tls = rustls::ServerConfig::builder()
97            .with_no_client_auth()
98            .with_single_cert(config.cert_chain, config.key_der)
99            .map_err(|e| ProxyError::TlsConfig(format!("server cert config: {e}")))?;
100
101        server_tls.alpn_protocols = advertised_alpns();
102        server_tls.max_early_data_size = u32::MAX;
103
104        let quic_server_config: quinn::crypto::rustls::QuicServerConfig =
105            server_tls.try_into().map_err(|e| ProxyError::TlsConfig(format!("{e}")))?;
106
107        let server_config = quinn::ServerConfig::with_crypto(Arc::new(quic_server_config));
108
109        let endpoint = quinn::Endpoint::server(server_config, config.bind_addr)
110            .map_err(|e| ProxyError::Listener(e.to_string()))?;
111
112        Ok(Self { endpoint })
113    }
114
115    /// Accept the next incoming connection and dispatch based on the
116    /// ALPN negotiated during the TLS handshake.
117    ///
118    /// Raw-QUIC connections are returned immediately with the negotiated
119    /// ALPN so the caller can pick the MoQT draft. For `h3` clients the
120    /// listener drives the HTTP/3 + extended-CONNECT handshake to
121    /// completion before returning a ready `wtransport::Connection`.
122    pub async fn accept(&self) -> Result<AcceptedConn, ProxyError> {
123        let incoming = self
124            .endpoint
125            .accept()
126            .await
127            .ok_or_else(|| ProxyError::Listener("endpoint closed".to_string()))?;
128
129        let mut connecting = incoming.accept().map_err(|e| ProxyError::Listener(e.to_string()))?;
130
131        // Peeking at handshake_data resolves as soon as the server has
132        // processed the ClientHello, so the ALPN is known before the
133        // full handshake completes — and the Connecting is still live.
134        let hs_data = connecting
135            .handshake_data()
136            .await
137            .map_err(|e| ProxyError::Listener(format!("handshake data: {e}")))?;
138
139        let alpn = hs_data
140            .downcast::<quinn::crypto::rustls::HandshakeData>()
141            .ok()
142            .and_then(|hd| hd.protocol)
143            .map(|p| p.to_vec())
144            .unwrap_or_default();
145
146        if alpn == H3_ALPN {
147            #[cfg(feature = "webtransport")]
148            {
149                let session_fut =
150                    wtransport::endpoint::IncomingSessionFuture::with_quic_connecting(connecting);
151                let session_request = session_fut
152                    .await
153                    .map_err(|e| ProxyError::Listener(format!("webtransport handshake: {e}")))?;
154                let conn = session_request
155                    .accept()
156                    .await
157                    .map_err(|e| ProxyError::Listener(format!("webtransport accept: {e}")))?;
158                Ok(AcceptedConn::WebTransport(conn))
159            }
160            #[cfg(not(feature = "webtransport"))]
161            {
162                drop(connecting);
163                Err(ProxyError::Listener(
164                    "client negotiated h3 but webtransport feature is not enabled".to_string(),
165                ))
166            }
167        } else {
168            let conn = connecting.await.map_err(|e| ProxyError::Listener(e.to_string()))?;
169            Ok(AcceptedConn::Quic { conn, alpn })
170        }
171    }
172
173    /// Get the local address this listener is bound to.
174    pub fn local_addr(&self) -> Result<SocketAddr, ProxyError> {
175        self.endpoint.local_addr().map_err(|e| ProxyError::Listener(e.to_string()))
176    }
177
178    /// Stop accepting new connections.
179    pub fn close(&self) {
180        self.endpoint.close(0u32.into(), b"proxy shutting down");
181    }
182}