iroh_relay/
quic.rs

1//! Create a QUIC server that accepts connections
2//! for QUIC address discovery.
3use std::{net::SocketAddr, sync::Arc};
4
5use n0_error::stack_error;
6use n0_future::time::Duration;
7use noq::{VarInt, crypto::rustls::QuicClientConfig};
8
9/// ALPN for our quic addr discovery
10pub const ALPN_QUIC_ADDR_DISC: &[u8] = b"/iroh-qad/0";
11/// Endpoint close error code
12pub const QUIC_ADDR_DISC_CLOSE_CODE: VarInt = VarInt::from_u32(1);
13/// Endpoint close reason
14pub const QUIC_ADDR_DISC_CLOSE_REASON: &[u8] = b"finished";
15
16#[cfg(feature = "server")]
17pub(crate) mod server {
18    use n0_error::e;
19    use noq::{
20        ApplicationClose, ConnectionError,
21        crypto::rustls::{NoInitialCipherSuite, QuicServerConfig},
22    };
23    use tokio::task::JoinSet;
24    use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle};
25    use tracing::{Instrument, debug, info, info_span};
26
27    use super::*;
28    use crate::server::Metrics;
29
30    pub(crate) struct QuicServer {
31        bind_addr: SocketAddr,
32        cancel: CancellationToken,
33        handle: AbortOnDropHandle<()>,
34    }
35
36    /// Server spawn errors
37    #[allow(missing_docs)]
38    #[stack_error(derive, add_meta)]
39    #[non_exhaustive]
40    pub enum QuicSpawnError {
41        #[error("TLS not configured")]
42        TlsNotConfigured {},
43        #[error(transparent)]
44        NoInitialCipherSuite {
45            #[error(std_err, from)]
46            source: NoInitialCipherSuite,
47        },
48        #[error("Unable to spawn a QUIC endpoint server")]
49        EndpointServer {
50            #[error(std_err)]
51            source: std::io::Error,
52        },
53        #[error("Unable to get the local address from the endpoint")]
54        LocalAddr {
55            #[error(std_err)]
56            source: std::io::Error,
57        },
58    }
59
60    impl QuicServer {
61        /// Returns a handle for this server.
62        ///
63        /// The server runs in the background as several async tasks.  This allows controlling
64        /// the server, in particular it allows gracefully shutting down the server.
65        pub(crate) fn handle(&self) -> ServerHandle {
66            ServerHandle {
67                cancel_token: self.cancel.clone(),
68            }
69        }
70
71        /// Returns the [`AbortOnDropHandle`] for the supervisor task managing the endpoint.
72        ///
73        /// This is the root of all the tasks for the QUIC address discovery service.  Aborting it will abort all the
74        /// other tasks for the service.  Awaiting it will complete when all the service tasks are
75        /// completed.[]
76        pub(crate) fn task_handle(&mut self) -> &mut AbortOnDropHandle<()> {
77            &mut self.handle
78        }
79
80        /// Returns the socket address for this QUIC server.
81        pub(crate) fn bind_addr(&self) -> SocketAddr {
82            self.bind_addr
83        }
84
85        /// Spawns a QUIC server that creates and QUIC endpoint and listens
86        /// for QUIC connections for address discovery
87        ///
88        /// # Errors
89        /// If the given `quic_config` contains a [`rustls::ServerConfig`] that cannot
90        /// be converted to a [`QuicServerConfig`], usually because it does not support
91        /// TLS 1.3, a [`NoInitialCipherSuite`] will occur.
92        ///
93        /// # Panics
94        /// If there is a panic during a connection, it will be propagated
95        /// up here. Any other errors in a connection will be logged as a
96        ///  warning.
97        pub(crate) fn spawn(
98            bind_addr: SocketAddr,
99            mut server_config: rustls::ServerConfig,
100            metrics: Arc<Metrics>,
101        ) -> Result<Self, QuicSpawnError> {
102            server_config.alpn_protocols = vec![crate::quic::ALPN_QUIC_ADDR_DISC.to_vec()];
103            let server_config = QuicServerConfig::try_from(server_config)?;
104            let mut server_config = noq::ServerConfig::with_crypto(Arc::new(server_config));
105            let transport_config =
106                Arc::get_mut(&mut server_config.transport).expect("not used yet");
107            transport_config
108                .max_concurrent_uni_streams(0_u8.into())
109                .max_concurrent_bidi_streams(0_u8.into())
110                // enable sending quic address discovery frames
111                .send_observed_address_reports(true);
112
113            let endpoint = noq::Endpoint::server(server_config, bind_addr)
114                .map_err(|err| e!(QuicSpawnError::EndpointServer, err))?;
115            let bind_addr = endpoint
116                .local_addr()
117                .map_err(|err| e!(QuicSpawnError::LocalAddr, err))?;
118
119            info!(?bind_addr, "QUIC server listening on");
120
121            let cancel = CancellationToken::new();
122            let cancel_accept_loop = cancel.clone();
123
124            let task = tokio::task::spawn(
125                async move {
126                    let mut set = JoinSet::new();
127                    debug!("waiting for connections...");
128                    loop {
129                        tokio::select! {
130                            biased;
131                            _ = cancel_accept_loop.cancelled() => {
132                                break;
133                            }
134                            Some(res) = set.join_next() => {
135                                if let Err(err) = res {
136                                    if err.is_panic() {
137                                        panic!("quic task panicked: {err:#?}");
138                                    } else {
139                                        debug!("quic task cancelled: {err:#?}");
140                                    }
141                                }
142                            }
143                            res = endpoint.accept() => match res {
144                                Some(incoming) => {
145                                     let remote_addr = incoming.remote_address();
146                                     set.spawn(
147                                         handle_connection(incoming, metrics.clone()).instrument(info_span!("qad-conn", %remote_addr))
148                                     );                                }
149                                None => {
150                                    debug!("endpoint closed");
151                                    break;
152                                }
153                            }
154                        }
155                    }
156                    // close all connections and wait until they have all grace
157                    // fully closed.
158                    endpoint.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
159                    endpoint.wait_idle().await;
160
161                    // all tasks should be closed, since the endpoint has shutdown
162                    // all connections, but await to ensure they are finished.
163                    set.abort_all();
164                    while !set.is_empty() {
165                        _ = set.join_next().await;
166                    }
167
168                    debug!("quic endpoint has been shutdown.");
169                }
170                .instrument(info_span!("quic-endpoint")),
171            );
172            Ok(Self {
173                bind_addr,
174                cancel,
175                handle: AbortOnDropHandle::new(task),
176            })
177        }
178
179        /// Closes the underlying QUIC endpoint and the tasks running the
180        /// QUIC connections.
181        pub(crate) async fn shutdown(mut self) {
182            self.cancel.cancel();
183            if !self.task_handle().is_finished() {
184                // only possible error is a `JoinError`, no errors about what might
185                // have happened during a connection are propagated.
186                _ = self.task_handle().await;
187            }
188        }
189    }
190
191    /// A handle for the Server side of QUIC address discovery.
192    ///
193    /// This does not allow access to the task but can communicate with it.
194    #[derive(Debug, Clone)]
195    pub(crate) struct ServerHandle {
196        cancel_token: CancellationToken,
197    }
198
199    impl ServerHandle {
200        /// Gracefully shut down the quic endpoint.
201        pub(crate) fn shutdown(&self) {
202            self.cancel_token.cancel()
203        }
204    }
205
206    /// Handle the connection from the client.
207    async fn handle_connection(
208        incoming: noq::Incoming,
209        metrics: Arc<Metrics>,
210    ) -> Result<(), ConnectionError> {
211        metrics.qad_incoming.inc();
212        debug!("incoming");
213        let connection = match incoming.await {
214            Ok(conn) => conn,
215            Err(e) => {
216                debug!("establishing failed: {e:#}");
217                metrics.qad_incoming_error.inc();
218                return Err(e);
219            }
220        };
221        metrics.qad_connections.inc();
222        debug!("established");
223        // wait for the client to close the connection
224        let connection_err = connection.closed().await;
225        metrics.qad_connections_closed.inc();
226        match connection_err {
227            noq::ConnectionError::ApplicationClosed(ApplicationClose { error_code, .. })
228                if error_code == QUIC_ADDR_DISC_CLOSE_CODE =>
229            {
230                debug!("peer disconnected");
231                Ok(())
232            }
233            _ => {
234                debug!("peer disconnected with {connection_err:#}");
235                metrics.qad_connections_errored.inc();
236                Err(connection_err)
237            }
238        }
239    }
240}
241
242/// Quic client related errors.
243#[allow(missing_docs)]
244#[stack_error(derive, add_meta, from_sources, std_sources)]
245#[non_exhaustive]
246pub enum Error {
247    #[error(transparent)]
248    Connect {
249        #[error(std_err)]
250        source: noq::ConnectError,
251    },
252    #[error(transparent)]
253    Connection {
254        #[error(std_err)]
255        source: noq::ConnectionError,
256    },
257    NoObservedAddr,
258}
259
260/// Handles the client side of QUIC address discovery.
261#[derive(Debug, Clone)]
262pub struct QuicClient {
263    /// A QUIC Endpoint.
264    ep: noq::Endpoint,
265    /// A client config.
266    client_config: noq::ClientConfig,
267}
268
269impl QuicClient {
270    /// Create a new QuicClient to handle the client side of QUIC
271    /// address discovery.
272    pub fn new(ep: noq::Endpoint, mut client_config: rustls::ClientConfig) -> Self {
273        // add QAD alpn
274        client_config.alpn_protocols = vec![ALPN_QUIC_ADDR_DISC.into()];
275        // go from rustls client config to rustls QUIC specific client config to
276        // a noq client config
277        let mut client_config = noq::ClientConfig::new(Arc::new(
278            QuicClientConfig::try_from(client_config).expect("known ciphersuite"),
279        ));
280
281        // enable the receive side of address discovery
282        let mut transport = noq_proto::TransportConfig::default();
283        // Setting the initial RTT estimate to a low value means
284        // we're sacrificing initial throughput, which is fine for
285        // QAD, which doesn't require us to have good initial throughput.
286        // It also implies a 999ms probe timeout, which means that
287        // if the packet gets lost (e.g. because we're probing ipv6, but
288        // ipv6 packets always get lost in our network configuration) we
289        // time out *closing the connection* after only 999ms.
290        // Even if the round trip time is bigger than 999ms, this doesn't
291        // prevent us from connecting, since that's dependent on the idle
292        // timeout (set to 30s by default).
293        transport.initial_rtt(Duration::from_millis(111));
294        transport.receive_observed_address_reports(true);
295
296        // keep it alive
297        transport.keep_alive_interval(Some(Duration::from_secs(25)));
298        transport.max_idle_timeout(Some(
299            Duration::from_secs(35).try_into().expect("known value"),
300        ));
301        client_config.transport_config(Arc::new(transport));
302
303        Self { ep, client_config }
304    }
305
306    /// Client side of QUIC address discovery.
307    ///
308    /// Creates a connection and returns the observed address
309    /// and estimated latency of the connection.
310    ///
311    /// Consumes and gracefully closes the connection.
312    #[cfg(all(test, feature = "server"))]
313    async fn get_addr_and_latency(
314        &self,
315        server_addr: SocketAddr,
316        host: &str,
317    ) -> Result<(SocketAddr, std::time::Duration), Error> {
318        use n0_future::StreamExt;
319        use noq_proto::PathId;
320
321        let connecting = self
322            .ep
323            .connect_with(self.client_config.clone(), server_addr, host);
324        let conn = connecting?.await?;
325        let mut external_addresses = conn.observed_external_addr();
326        // TODO(ramfox): I'd like to be able to cancel this so we can close cleanly
327        // if there the task that runs this function gets aborted.
328        // tokio::select! {
329        //     _ = cancel.cancelled() => {
330        //         conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
331        //         bail_any!("QUIC address discovery canceled early");
332        //     },
333        //     res = external_addresses.wait_for(|addr| addr.is_some()) => {
334        //         let addr = res?.expect("checked");
335        //         let latency = conn.rtt() / 2;
336        //         // gracefully close the connections
337        //         conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
338        //         Ok((addr, latency))
339        //     }
340
341        let Some(mut observed_addr) = external_addresses.next().await else {
342            n0_error::bail!(Error::NoObservedAddr);
343        };
344        // if we've sent to an ipv4 address, but received an observed address
345        // that is ivp6 then the address is an [IPv4-Mapped IPv6 Addresses](https://doc.rust-lang.org/beta/std/net/struct.Ipv6Addr.html#ipv4-mapped-ipv6-addresses)
346        observed_addr = SocketAddr::new(observed_addr.ip().to_canonical(), observed_addr.port());
347        let latency = conn.rtt(PathId::ZERO).unwrap_or_default();
348        // gracefully close the connections
349        conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
350        Ok((observed_addr, latency))
351    }
352
353    /// Create a connection usable for qad
354    pub async fn create_conn(
355        &self,
356        server_addr: SocketAddr,
357        host: &str,
358    ) -> Result<noq::Connection, Error> {
359        let config = self.client_config.clone();
360        let connecting = self.ep.connect_with(config, server_addr, host);
361        let conn = connecting?.await?;
362        Ok(conn)
363    }
364}
365
366#[cfg(all(test, feature = "server", with_crypto_provider))]
367mod tests {
368    use std::{net::Ipv4Addr, sync::Arc};
369
370    use n0_error::{Result, StdResultExt};
371    use n0_future::{
372        task::AbortOnDropHandle,
373        time::{self, Instant},
374    };
375    use n0_tracing_test::traced_test;
376    use noq::crypto::rustls::QuicServerConfig;
377    use tracing::{Instrument, debug, info, info_span};
378    use webpki_types::PrivatePkcs8KeyDer;
379
380    use super::*;
381
382    #[tokio::test]
383    #[traced_test]
384    #[cfg(feature = "test-utils")]
385    async fn quic_endpoint_basic() -> Result {
386        use super::server::QuicServer;
387
388        let host: Ipv4Addr = "127.0.0.1".parse().unwrap();
389        // create a server config with self signed certificates
390        let (_, server_config) = super::super::server::testing::self_signed_tls_certs_and_config();
391        let bind_addr = SocketAddr::new(host.into(), 0);
392        let quic_server = QuicServer::spawn(bind_addr, server_config, Default::default())?;
393
394        // create a client-side endpoint
395        let client_endpoint =
396            noq::Endpoint::client(SocketAddr::new(host.into(), 0)).std_context("client")?;
397        let client_addr = client_endpoint.local_addr().std_context("local addr")?;
398
399        // create the client configuration used for the client endpoint when they
400        // initiate a connection with the server
401        let client_config = crate::tls::make_dangerous_client_config();
402        let quic_client = QuicClient::new(client_endpoint.clone(), client_config);
403
404        let (addr, _latency) = quic_client
405            .get_addr_and_latency(quic_server.bind_addr(), &host.to_string())
406            .await?;
407
408        // wait until the endpoint delivers the closing message to the server
409        client_endpoint.wait_idle().await;
410        // shut down the quic server
411        quic_server.shutdown().await;
412
413        assert_eq!(client_addr, addr);
414        Ok(())
415    }
416
417    #[tokio::test(start_paused = true)]
418    #[traced_test]
419    async fn test_qad_client_closes_unresponsive_fast() -> Result {
420        // create a client-side endpoint
421        let client_endpoint = noq::Endpoint::client(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0))
422            .std_context("client")?;
423
424        // create an socket that does not respond.
425        let server_socket =
426            tokio::net::UdpSocket::bind(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0))
427                .await
428                .std_context("bind")?;
429        let server_addr = server_socket.local_addr().std_context("local addr")?;
430
431        // create the client configuration used for the client endpoint when they
432        // initiate a connection with the server
433        let client_config = crate::tls::make_dangerous_client_config();
434        let quic_client = QuicClient::new(client_endpoint.clone(), client_config);
435
436        // Start a connection attempt with nirvana - this will fail
437        let task = AbortOnDropHandle::new(tokio::spawn({
438            async move {
439                quic_client
440                    .get_addr_and_latency(server_addr, "localhost")
441                    .await
442            }
443        }));
444
445        // Even if we wait longer than the probe timeout, we will still be attempting to connect:
446        tokio::time::sleep(Duration::from_millis(1000)).await;
447        assert!(!task.is_finished());
448
449        // time the closing of the client endpoint
450        let before = Instant::now();
451        client_endpoint.close(0u32.into(), b"byeeeee");
452        client_endpoint.wait_idle().await;
453        let time = Instant::now().duration_since(before);
454
455        assert_eq!(time, Duration::from_millis(999));
456
457        Ok(())
458    }
459
460    /// Makes sure that, even though the RTT was set to some fairly low value,
461    /// we *do* try to connect for longer than what the time out would be after closing
462    /// the connection, when we *don't* close the connection.
463    ///
464    /// In this case we don't simulate it via synthetically high RTT, but by dropping
465    /// all packets on the server-side for 2 seconds.
466    #[tokio::test]
467    #[traced_test]
468    async fn test_qad_connect_delayed() -> Result {
469        // Create a socket for our QAD server.  We need the socket separately because we
470        // need to pop off messages before we attach it to the Noq Endpoint.
471        let socket = tokio::net::UdpSocket::bind(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0))
472            .await
473            .std_context("bind")?;
474        let server_addr = socket.local_addr().std_context("local addr")?;
475        info!(addr = ?server_addr, "server socket bound");
476
477        // Create a QAD server with a self-signed cert, all manually.
478        let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()])
479            .std_context("self signed")?;
480        let key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der());
481        let mut server_crypto = rustls::ServerConfig::builder_with_provider(Arc::new(
482            rustls::crypto::ring::default_provider(),
483        ))
484        .with_safe_default_protocol_versions()
485        .std_context("crypto provider")?
486        .with_no_client_auth()
487        .with_single_cert(vec![cert.cert.into()], key.into())
488        .std_context("tls")?;
489        server_crypto.key_log = Arc::new(rustls::KeyLogFile::new());
490        server_crypto.alpn_protocols = vec![ALPN_QUIC_ADDR_DISC.to_vec()];
491        let mut server_config = noq::ServerConfig::with_crypto(Arc::new(
492            QuicServerConfig::try_from(server_crypto).std_context("config")?,
493        ));
494        let transport_config = Arc::get_mut(&mut server_config.transport).unwrap();
495        transport_config.send_observed_address_reports(true);
496
497        let start = Instant::now();
498        let server_task = tokio::spawn(
499            async move {
500                info!("Dropping all packets");
501                time::timeout(Duration::from_secs(2), async {
502                    let mut buf = [0u8; 1500];
503                    loop {
504                        let (len, src) = socket.recv_from(&mut buf).await.unwrap();
505                        debug!(%len, ?src, "Dropped a packet");
506                    }
507                })
508                .await
509                .ok();
510                info!("starting server");
511                let server = noq::Endpoint::new(
512                    Default::default(),
513                    Some(server_config),
514                    socket.into_std().unwrap(),
515                    Arc::new(noq::TokioRuntime),
516                )
517                .std_context("endpoint new")?;
518                info!("accepting conn");
519                let incoming = server.accept().await.expect("missing conn");
520                info!("incoming!");
521                let conn = incoming.await.std_context("incoming")?;
522                conn.closed().await;
523                server.wait_idle().await;
524                n0_error::Ok(())
525            }
526            .instrument(info_span!("server")),
527        );
528        let server_task = AbortOnDropHandle::new(server_task);
529
530        info!("starting client");
531        let client_endpoint = noq::Endpoint::client(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0))
532            .std_context("client")?;
533
534        // create the client configuration used for the client endpoint when they
535        // initiate a connection with the server
536        let client_config = crate::tls::make_dangerous_client_config();
537        let quic_client = QuicClient::new(client_endpoint.clone(), client_config);
538
539        // Now we should still connect, but it should take more than 1s.
540        info!("making QAD request");
541        let (addr, latency) = time::timeout(
542            Duration::from_secs(10),
543            quic_client.get_addr_and_latency(server_addr, "localhost"),
544        )
545        .await
546        .std_context("timeout")??;
547        let duration = start.elapsed();
548        info!(?duration, ?addr, ?latency, "QAD succeeded");
549        assert!(duration >= Duration::from_secs(1));
550
551        time::timeout(Duration::from_secs(10), server_task)
552            .await
553            .std_context("timeout")?
554            .std_context("server task")??;
555
556        Ok(())
557    }
558}