//! Tools for spawning an accept loop that routes incoming requests to the right protocol. //! //! ## Example //! //! ```no_run //! # #[cfg(with_crypto_provider)] //! # use iroh::{ //! # endpoint::{BindError, presets}, //! # protocol::Router, //! # Endpoint, //! # }; //! # use iroh::{ //! # endpoint::Connection, //! # protocol::{AcceptError, ProtocolHandler}, //! # }; //! # //! # #[cfg(with_crypto_provider)] //! # async fn test_compile() -> Result<(), BindError> { //! let endpoint = Endpoint::bind(presets::N0).await?; //! //! let router = Router::builder(endpoint).accept(b"/my/alpn", Echo).spawn(); //! # Ok(()) //! # } //! //! // The protocol definition: //! #[derive(Debug, Clone)] //! struct Echo; //! //! impl ProtocolHandler for Echo { //! async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { //! let (mut send, mut recv) = connection.accept_bi().await?; //! //! // Echo any bytes received back directly. //! let bytes_sent = tokio::io::copy(&mut recv, &mut send).await?; //! //! send.finish()?; //! connection.closed().await; //! //! Ok(()) //! } //! } //! ``` use std::{ collections::BTreeMap, future::Future, pin::Pin, sync::{Arc, Mutex}, }; use n0_error::{AnyError, e, stack_error}; use n0_future::{ join_all, task::{self, AbortOnDropHandle, JoinSet}, }; use tokio_util::sync::CancellationToken; use tracing::{Instrument, debug, error, field::Empty, info_span, trace, warn}; use crate::{ Endpoint, endpoint::{Accepting, Connection, RemoteEndpointIdError, quic}, }; /// The built router. /// /// Construct this using [`Router::builder`]. /// /// When dropped, this will abort listening the tasks, so make sure to store it. /// /// Even with this abort-on-drop behaviour, it's recommended to call and await /// [`Router::shutdown`] before ending the process. /// /// As an example for graceful shutdown, e.g. for tests or CLI tools, /// wait for [`tokio::signal::ctrl_c()`]: /// /// ```no_run /// # #[cfg(with_crypto_provider)] /// # { /// # use std::sync::Arc; /// # use n0_error::StdResultExt; /// # use iroh::{endpoint::{Connecting, presets}, protocol::{ProtocolHandler, Router}, Endpoint, EndpointAddr}; /// # /// # async fn test_compile() -> n0_error::Result<()> { /// let endpoint = Endpoint::bind(presets::N0).await?; /// /// let router = Router::builder(endpoint) /// // .accept(&ALPN, ) /// .spawn(); /// /// // wait until the user wants to /// tokio::signal::ctrl_c().await.std_context("ctrl+c")?; /// router.shutdown().await.std_context("shutdown")?; /// # Ok(()) /// # } /// # } /// ``` #[derive(Clone, Debug)] pub struct Router { endpoint: Endpoint, // `Router` needs to be `Clone + Send`, and we need to `task.await` in its `shutdown()` impl. task: Arc>>>, cancel_token: CancellationToken, } /// Builder for creating a [`Router`] for accepting protocols. #[derive(derive_more::Debug)] pub struct RouterBuilder { endpoint: Endpoint, protocols: ProtocolMap, #[debug(skip)] incoming_filter: Option, } #[allow(missing_docs)] #[stack_error(derive, add_meta, from_sources, std_sources)] #[non_exhaustive] pub enum AcceptError { #[error(transparent)] Connecting { source: crate::endpoint::ConnectingError, }, #[error(transparent)] Connection { source: crate::endpoint::ConnectionError, }, #[error(transparent)] MissingRemoteEndpointId { source: RemoteEndpointIdError }, #[error("Not allowed.")] NotAllowed {}, #[error(transparent)] User { source: AnyError }, } impl AcceptError { /// Creates a new user error from an arbitrary error type. // TODO(Frando): Rename to `from_std` #[track_caller] pub fn from_err(value: T) -> Self { e!(AcceptError::User { source: AnyError::from_std(value) }) } /// Creates a new user error from an arbitrary boxed error. #[track_caller] pub fn from_boxed(value: Box) -> Self { e!(AcceptError::User { source: AnyError::from_std_box(value) }) } } impl From for AcceptError { fn from(err: std::io::Error) -> Self { Self::from_err(err) } } impl From for AcceptError { fn from(err: quic::ClosedStream) -> Self { Self::from_err(err) } } /// Verdict from a [`IncomingFilter`] for an incoming connection. /// /// The filter can accept the connection, send a retry token to validate /// the source address, actively refuse the connection, or silently drop it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum IncomingFilterOutcome { /// Accept the connection. Accept, /// Tell the remote to retry with a token (a QUIC `RETRY` packet). /// /// What this does depends on the connection type: /// /// - **Direct (UDP) connections**: this is QUIC source address /// validation. If the socket address was spoofed, the retry token is /// sent to the spoofed address, so we never hear from the attacker /// again. If the address was real, the client repeats the connection /// attempt with the token, and the next [`Incoming`] for that flow has /// [`Incoming::remote_addr_validated`] set to `true`. The token is /// bound to the source address. /// /// - **Relay connections**: there is no source address to validate /// (the relay already vouches for the packet origin), so the /// "validation" itself has no security meaning. However, the retry /// still imposes a real cost on the client: an extra round trip /// through the relay plus the work of sending a fresh ClientHello /// with the token. This filters out adversarial clients that /// don't bother to handle retry tokens, and adds latency before /// we get to the more expensive part of the handshake. The next /// [`Incoming`] for that flow will also have /// [`Incoming::remote_addr_validated`] set to `true`, but again, /// that just means the client cooperated with the retry. /// /// In short: for direct connections, `Retry` is address validation; /// for relay connections, it is a cost-imposition mechanism. /// /// [`Incoming`]: crate::endpoint::Incoming /// [`Incoming::remote_addr_validated`]: crate::endpoint::Incoming::remote_addr_validated Retry, /// Actively refuse the connection. The remote will receive a /// CONNECTION_REFUSED error immediately. Reject, /// Ignore the connection entirely. The remote gets no response and will /// eventually time out. Ignore, } /// Filter predicate used for early filtering of incoming connections before the handshake completes. /// /// See [`RouterBuilder::incoming_filter`] for more details. pub type IncomingFilter = Arc IncomingFilterOutcome + Send + Sync + 'static>; /// Handler for incoming connections. /// /// A router accepts connections for arbitrary ALPN protocols. /// /// With this trait, you can handle incoming connections for any protocol. /// /// Implement this trait on a struct that should handle incoming connections. /// The protocol handler must then be registered on the endpoint for an ALPN protocol with /// [`crate::protocol::RouterBuilder::accept`]. /// /// See the [module documentation](crate::protocol) for an example. pub trait ProtocolHandler: Send + Sync + std::fmt::Debug + 'static { /// Optional interception point to handle the `Accepting` state. /// /// Can be implemented as `async fn on_accepting(&self, accepting: Accepting) -> Result`. /// /// Typically, this method is used as an early interception point to accept /// or reject a connection. /// /// However, this method can also be used to implement the accept side of a /// 0-RTT connection. /// /// ## 0-RTT /// /// `ProtocolHandler::on_accepting` allows you to take over the connection /// state machine early in the handshake processes, by calling [`Accepting::into_0rtt`]. /// /// When working with 0-RTT, you may want to implement all of your protocol /// logic in `on_accepting`. This is fine because `on_accepting` can handle /// long-running processes. In this case, the [`ProtocolHandler::accept`] method /// can simply return `Ok(())`. fn on_accepting( &self, accepting: Accepting, ) -> impl Future> + Send { async move { let conn = accepting.await?; Ok(conn) } } /// Handle an incoming connection. /// /// Can be implemented as `async fn accept(&self, connection: Connection) -> Result<()>`. /// /// The returned future runs on a freshly spawned tokio task so it can be long-running. Once /// `accept()` returns, the connection is dropped. This means that it will be closed /// if there are no other clones of the connection. If there is a protocol error, you /// can use [`Connection::close`] to send an error code to the remote peer. Returning /// an `Err` will also drop the connection and log a warning, but no /// dedicated error code will be sent to the peer, so it's recommended to explicitly /// close the connection within your accept handler. /// /// When [`Router::shutdown`] is called, no further connections will be accepted, and /// the futures returned by [`Self::accept`] will be aborted after the future returned /// from [`ProtocolHandler::shutdown`] completes. fn accept( &self, connection: Connection, ) -> impl Future> + Send; /// Called when the router shuts down. /// /// Can be implemented as `async fn shutdown(&self)`. /// /// This is called from [`Router::shutdown`]. The returned future is awaited before /// the router closes the endpoint. fn shutdown(&self) -> impl Future + Send { async move {} } } impl ProtocolHandler for Arc { async fn on_accepting(&self, accepting: Accepting) -> Result { self.as_ref().on_accepting(accepting).await } async fn accept(&self, conn: Connection) -> Result<(), AcceptError> { self.as_ref().accept(conn).await } async fn shutdown(&self) { self.as_ref().shutdown().await } } impl ProtocolHandler for Box { async fn on_accepting(&self, accepting: Accepting) -> Result { self.as_ref().on_accepting(accepting).await } async fn accept(&self, conn: Connection) -> Result<(), AcceptError> { self.as_ref().accept(conn).await } async fn shutdown(&self) { self.as_ref().shutdown().await } } impl From for Box { fn from(value: T) -> Self { Box::new(value) } } /// A dyn-compatible version of [`ProtocolHandler`] that returns boxed futures. /// /// Any type that implements [`ProtocolHandler`] automatically also implements [`DynProtocolHandler`]. /// There is a also [`From`] impl to turn any type that implements [`ProtocolHandler`] into a /// `Box`. // // We are not using [`n0_future::boxed::BoxFuture] because we don't need a `'static` bound // on these futures. pub trait DynProtocolHandler: Send + Sync + std::fmt::Debug + 'static { /// See [`ProtocolHandler::on_accepting`]. fn on_accepting( &self, accepting: Accepting, ) -> Pin> + Send + '_>> { Box::pin(async move { let conn = accepting.await?; Ok(conn) }) } /// See [`ProtocolHandler::accept`]. fn accept( &self, connection: Connection, ) -> Pin> + Send + '_>>; /// See [`ProtocolHandler::shutdown`]. fn shutdown(&self) -> Pin + Send + '_>> { Box::pin(async move {}) } } impl DynProtocolHandler for P { fn accept( &self, connection: Connection, ) -> Pin> + Send + '_>> { Box::pin(::accept(self, connection)) } fn on_accepting( &self, accepting: Accepting, ) -> Pin> + Send + '_>> { Box::pin(::on_accepting(self, accepting)) } fn shutdown(&self) -> Pin + Send + '_>> { Box::pin(::shutdown(self)) } } /// A typed map of protocol handlers, mapping them from ALPNs. #[derive(Debug, Default)] pub(crate) struct ProtocolMap(BTreeMap, Box>); impl ProtocolMap { /// Returns the registered protocol handler for an ALPN as a [`Arc`]. pub(crate) fn get(&self, alpn: &[u8]) -> Option<&dyn DynProtocolHandler> { self.0.get(alpn).map(|p| &**p) } /// Inserts a protocol handler. pub(crate) fn insert(&mut self, alpn: Vec, handler: Box) { self.0.insert(alpn, handler); } /// Returns an iterator of all registered ALPN protocol identifiers. pub(crate) fn alpns(&self) -> impl Iterator> { self.0.keys() } /// Shuts down all protocol handlers. /// /// Calls and awaits [`ProtocolHandler::shutdown`] for all registered handlers concurrently. pub(crate) async fn shutdown(&self) { let handlers = self.0.values().map(|p| p.shutdown()); join_all(handlers).await; } } impl Router { /// Creates a new [`Router`] using given [`Endpoint`]. pub fn builder(endpoint: Endpoint) -> RouterBuilder { RouterBuilder::new(endpoint) } /// Returns the [`Endpoint`] stored in this router. pub fn endpoint(&self) -> &Endpoint { &self.endpoint } /// Checks if the router is already shutdown. pub fn is_shutdown(&self) -> bool { self.cancel_token.is_cancelled() } /// Shuts down the accept loop cleanly. /// /// When this function returns, all [`ProtocolHandler`]s will be shutdown and /// `Endpoint::close` will have been called. /// /// If already shutdown, it returns `Ok`. /// /// If some [`ProtocolHandler`] panicked in the accept loop, this will propagate /// that panic into the result here. pub async fn shutdown(&self) -> Result<(), n0_future::task::JoinError> { if self.is_shutdown() { return Ok(()); } // Trigger shutdown of the main run task by activating the cancel token. self.cancel_token.cancel(); // Wait for the main task to terminate. // MutexGuard is not held across await point let task = self.task.lock().expect("poisoned").take(); if let Some(task) = task { task.await?; } Ok(()) } } impl RouterBuilder { /// Creates a new router builder using given [`Endpoint`]. pub fn new(endpoint: Endpoint) -> Self { Self { endpoint, protocols: ProtocolMap::default(), incoming_filter: None, } } /// Sets a filter that decides whether to accept an incoming connection before the /// TLS handshake completes. /// /// The filter is called with the raw [`Incoming`] for each connection attempt /// and returns an [`IncomingFilterOutcome`] that determines what happens next. /// /// Implementers have full access to the [`Incoming`] and can use any of its /// methods (including [`Incoming::decrypt`]) to make their decision. Note that /// `decrypt()` is relatively expensive, so filters should reject based on /// cheaper signals (e.g. remote address) first. /// /// [`Incoming`]: crate::endpoint::Incoming /// [`Incoming::decrypt`]: crate::endpoint::Incoming::decrypt pub fn incoming_filter(mut self, filter: IncomingFilter) -> Self { self.incoming_filter = Some(filter); self } /// Configures the router to accept the [`ProtocolHandler`] when receiving a connection /// with this `alpn`. /// /// `handler` can either be a type that implements [`ProtocolHandler`] or a /// [`Box`]. /// /// [`Box`]: DynProtocolHandler pub fn accept( mut self, alpn: impl AsRef<[u8]>, handler: impl Into>, ) -> Self { self.protocols .insert(alpn.as_ref().to_vec(), handler.into()); self } /// Returns the [`Endpoint`] stored in this builder. pub fn endpoint(&self) -> &Endpoint { &self.endpoint } /// Spawns an accept loop and returns a handle to it encapsulated as the [`Router`]. #[must_use = "Router aborts when dropped, use Router::shutdown to shut the router down cleanly"] pub fn spawn(self) -> Router { // Update the endpoint with our alpns. let alpns = self .protocols .alpns() .map(|alpn| alpn.to_vec()) .collect::>(); let protocols = Arc::new(self.protocols); let incoming_filter = self.incoming_filter; self.endpoint.set_alpns(alpns); let mut join_set = JoinSet::new(); let endpoint = self.endpoint.clone(); // Our own shutdown works with a cancellation token. let cancel = CancellationToken::new(); let cancel_token = cancel.clone(); let run_loop_fut = async move { // Make sure to cancel the token, if this future ever exits. let _cancel_guard = cancel_token.clone().drop_guard(); // We create a separate cancellation token to stop any `ProtocolHandler::accept` futures // that are still running after `ProtocolHandler::shutdown` was called. let handler_cancel_token = CancellationToken::new(); loop { tokio::select! { biased; _ = cancel_token.cancelled() => { break; }, // handle task terminations and quit on panics. Some(res) = join_set.join_next() => { match res { Err(outer) => { if outer.is_panic() { error!("Task panicked: {outer:?}"); break; } else if outer.is_cancelled() { trace!("Task cancelled: {outer:?}"); } else { error!("Task failed: {outer:?}"); break; } } Ok(Some(())) => { trace!("Task finished"); } Ok(None) => { trace!("Task cancelled"); } } }, // handle incoming p2p connections. incoming = endpoint.accept() => { let Some(incoming) = incoming else { break; // Endpoint is closed. }; if let Some(filter) = &incoming_filter { match filter(&incoming) { IncomingFilterOutcome::Accept => {} IncomingFilterOutcome::Retry => { if incoming.remote_addr_validated() { debug!( "filter returned Retry for an already validated connection", ); } if let Err(err) = incoming.retry() { err.into_incoming().refuse(); } continue; } IncomingFilterOutcome::Reject => { incoming.refuse(); continue; } IncomingFilterOutcome::Ignore => { incoming.ignore(); continue; } } } let protocols = protocols.clone(); let token = handler_cancel_token.child_token(); let span = info_span!("router.accept", me=%endpoint.id().fmt_short(), remote=Empty, alpn=Empty); join_set.spawn(async move { token.run_until_cancelled(handle_connection(incoming, protocols)).await }.instrument(span)); }, } } // We first shutdown the protocol handlers to give them a chance to close connections gracefully. protocols.shutdown().await; // We now cancel the remaining `ProtocolHandler::accept` futures. handler_cancel_token.cancel(); // Now we close the endpoint. This will force-close all connections that are not yet closed. endpoint.close().await; // Finally, we abort the remaining accept tasks. This should be a noop because we already cancelled // the futures above. tracing::debug!("Shutting down remaining tasks"); join_set.abort_all(); while let Some(res) = join_set.join_next().await { match res { Err(err) if err.is_panic() => error!("Task panicked: {err:?}"), _ => {} } } }; let task = task::spawn(run_loop_fut.instrument(tracing::Span::current())); let task = AbortOnDropHandle::new(task); Router { endpoint: self.endpoint, task: Arc::new(Mutex::new(Some(task))), cancel_token: cancel, } } } async fn handle_connection(incoming: crate::endpoint::Incoming, protocols: Arc) { let mut accepting = match incoming.accept() { Ok(conn) => conn, Err(err) => { warn!("Ignoring connection: accepting failed: {err:#}"); return; } }; let alpn = match accepting.alpn().await { Ok(alpn) => alpn, Err(err) => { warn!("Ignoring connection: invalid handshake: {err:#}"); return; } }; tracing::Span::current().record("alpn", String::from_utf8_lossy(&alpn).to_string()); let Some(handler) = protocols.get(&alpn) else { warn!("Ignoring connection: unsupported ALPN protocol"); return; }; match handler.on_accepting(accepting).await { Ok(connection) => { tracing::Span::current().record( "remote", tracing::field::display(connection.remote_id().fmt_short()), ); if let Err(err) = handler.accept(connection).await { warn!("Handling incoming connection ended with error: {err}"); } } Err(err) => { warn!("Accepting incoming connection ended with error: {err}"); } } } #[cfg(all(test, with_crypto_provider))] mod tests { use std::{sync::Mutex, time::Duration}; use n0_error::{Result, StdResultExt}; use n0_tracing_test::traced_test; use super::*; use crate::endpoint::{ ApplicationClose, BeforeConnectOutcome, ConnectError, ConnectWithOptsError, ConnectionError, EndpointHooks, presets, }; #[tokio::test] async fn test_shutdown() -> Result { let endpoint = Endpoint::bind(presets::Minimal).await?; let router = Router::builder(endpoint.clone()).spawn(); assert!(!router.is_shutdown()); assert!(!endpoint.is_closed()); router.shutdown().await.anyerr()?; assert!(router.is_shutdown()); assert!(endpoint.is_closed()); Ok(()) } // The protocol definition: #[derive(Debug, Clone)] struct Echo; const ECHO_ALPN: &[u8] = b"/iroh/echo/1"; impl ProtocolHandler for Echo { async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { println!("accepting echo"); let (mut send, mut recv) = connection.accept_bi().await?; // Echo any bytes received back directly. let _bytes_sent = tokio::io::copy(&mut recv, &mut send).await?; send.finish()?; connection.closed().await; Ok(()) } } #[tokio::test] async fn test_limiter_hook() -> Result { // tracing_subscriber::fmt::try_init().ok(); #[derive(Debug, Default)] struct LimitHook; impl EndpointHooks for LimitHook { async fn before_connect<'a>( &'a self, _remote_addr: &'a iroh_base::EndpointAddr, alpn: &'a [u8], ) -> BeforeConnectOutcome { assert_eq!(alpn, ECHO_ALPN); // deny all access BeforeConnectOutcome::Reject } } let e1 = Endpoint::bind(presets::Minimal).await?; let r1 = Router::builder(e1.clone()).accept(ECHO_ALPN, Echo).spawn(); let addr1 = r1.endpoint().addr(); dbg!(&addr1); let e2 = Endpoint::builder(presets::Minimal) .hooks(LimitHook) .bind() .await?; println!("connecting"); let conn_err = e2.connect(addr1, ECHO_ALPN).await.unwrap_err(); assert!(matches!( conn_err, ConnectError::Connect { source: ConnectWithOptsError::LocallyRejected { .. }, .. } )); r1.shutdown().await.anyerr()?; e2.close().await; Ok(()) } /// Test that `Accepting::remote_addr()` is consistent with `Incoming::remote_addr()`. #[tokio::test] #[traced_test] async fn test_accepting_remote_addr() -> Result { use crate::endpoint::{IncomingAddr, presets}; let e1 = Endpoint::builder(presets::Minimal) .alpns(vec![ECHO_ALPN.to_vec()]) .bind() .await?; let addr1 = e1.addr(); let e2 = Endpoint::bind(presets::Minimal).await?; // Spawn the client connect so it runs concurrently with accept. let connect_task = tokio::spawn({ let addr1 = addr1.clone(); let e2 = e2.clone(); async move { e2.connect(addr1, ECHO_ALPN).await } }); let incoming = e1.accept().await.expect("accept"); let incoming_addr = incoming.remote_addr(); assert!(matches!(incoming_addr, IncomingAddr::Ip(_))); let accepting = incoming.accept().anyerr()?; assert_eq!(incoming_addr, accepting.remote_addr()); // Clean up. drop(accepting); drop(connect_task); e1.close().await; e2.close().await; Ok(()) } mod incoming_filter { use std::{ sync::{ Arc, atomic::{AtomicBool, Ordering::Relaxed}, }, time::Duration, }; use n0_error::{Result, StdResultExt}; use n0_tracing_test::traced_test; use crate::{ Endpoint, EndpointAddr, endpoint::presets, protocol::{ IncomingFilterOutcome, Router, tests::{ECHO_ALPN, Echo}, }, }; /// Two direct endpoints with a filtered router on the first. /// /// Binds to IPv4 loopback only so retry-token validation works on /// multi-homed CI hosts (tokens are tied to the source address). async fn direct_pair(filter: F) -> Result<(Router, Endpoint, EndpointAddr)> where F: Fn(&crate::endpoint::Incoming) -> IncomingFilterOutcome + Send + Sync + 'static, { let e1 = Endpoint::builder(presets::Minimal) .clear_ip_transports() .bind_addr((std::net::Ipv4Addr::LOCALHOST, 0)) .anyerr()? .bind() .await?; let r1 = Router::builder(e1.clone()) .incoming_filter(Arc::new(filter)) .accept(ECHO_ALPN, Echo) .spawn(); let addr = r1.endpoint().addr(); let e2 = Endpoint::builder(presets::Minimal) .clear_ip_transports() .bind_addr((std::net::Ipv4Addr::LOCALHOST, 0)) .anyerr()? .bind() .await?; Ok((r1, e2, addr)) } /// Two relay-only endpoints with a filtered router on the first. async fn relay_pair( filter: F, ) -> Result<(Router, Endpoint, EndpointAddr, impl std::any::Any)> where F: Fn(&crate::endpoint::Incoming) -> IncomingFilterOutcome + Send + Sync + 'static, { let (_relay_map, relay_url, guard) = crate::test_utils::run_relay_server().await.anyerr()?; let relay_mode = crate::RelayMode::Custom(crate::RelayMap::from(relay_url.clone())); let e1 = Endpoint::builder(presets::Minimal) .relay_mode(relay_mode.clone()) .ca_tls_config(crate::tls::CaTlsConfig::insecure_skip_verify()) .bind() .await?; let r1 = Router::builder(e1.clone()) .incoming_filter(Arc::new(filter)) .accept(ECHO_ALPN, Echo) .spawn(); let addr = EndpointAddr::new(e1.id()).with_relay_url(relay_url); let e2 = Endpoint::builder(presets::Minimal) .relay_mode(relay_mode) .ca_tls_config(crate::tls::CaTlsConfig::insecure_skip_verify()) .bind() .await?; Ok((r1, e2, addr, guard)) } #[tokio::test] #[traced_test] async fn addr_retry() -> Result { let (r1, e2, addr) = direct_pair(|incoming| { if !incoming.remote_addr_validated() { IncomingFilterOutcome::Retry } else { IncomingFilterOutcome::Accept } }) .await?; // Server sends retry (unvalidated), then accepts once validated. assert!(e2.connect(addr, ECHO_ALPN).await.is_ok()); r1.shutdown().await.anyerr()?; e2.close().await; Ok(()) } #[tokio::test] #[traced_test] async fn addr_reject() -> Result { let (r1, e2, addr) = direct_pair(|_| IncomingFilterOutcome::Reject).await?; assert!(e2.connect(addr, ECHO_ALPN).await.is_err()); r1.shutdown().await.anyerr()?; e2.close().await; Ok(()) } #[tokio::test] #[traced_test] async fn addr_ignore() -> Result { let (r1, e2, addr) = direct_pair(|_| IncomingFilterOutcome::Ignore).await?; // No response at all — connect times out. let result = tokio::time::timeout(Duration::from_millis(500), e2.connect(addr, ECHO_ALPN)).await; assert!(result.is_err(), "expected timeout"); r1.shutdown().await.anyerr()?; e2.close().await; Ok(()) } #[tokio::test] #[traced_test] async fn relay_reject() -> Result { let (r1, e2, addr, _guard) = relay_pair(|_| IncomingFilterOutcome::Reject).await?; assert!(e2.connect(addr, ECHO_ALPN).await.is_err()); r1.shutdown().await.anyerr()?; e2.close().await; Ok(()) } #[tokio::test] #[traced_test] async fn relay_ignore() -> Result { let (r1, e2, addr, _guard) = relay_pair(|_| IncomingFilterOutcome::Ignore).await?; let result = tokio::time::timeout(Duration::from_millis(500), e2.connect(addr, ECHO_ALPN)).await; assert!(result.is_err(), "expected timeout"); r1.shutdown().await.anyerr()?; e2.close().await; Ok(()) } /// Verify that returning `Retry` for a direct connection causes the /// remote to retry with a token, after which `validated` is true. #[tokio::test] #[traced_test] async fn addr_retry_then_validated() -> Result { let saw_validated = Arc::::default(); let saw_unvalidated = Arc::::default(); let (sv, su) = (saw_validated.clone(), saw_unvalidated.clone()); let (r1, e2, addr) = direct_pair(move |incoming| { if incoming.remote_addr_validated() { sv.store(true, Relaxed); IncomingFilterOutcome::Accept } else { su.store(true, Relaxed); IncomingFilterOutcome::Retry } }) .await?; // The connection should now succeed: first attempt returns Retry, // the client retries with the token, the second attempt is // validated and accepted. let _conn = e2.connect(addr, ECHO_ALPN).await?; assert!(saw_unvalidated.load(Relaxed)); assert!(saw_validated.load(Relaxed)); r1.shutdown().await.anyerr()?; e2.close().await; Ok(()) } /// Verify that returning `Retry` for a relay connection also causes /// the remote to retry with a token. The "validation" has no /// security meaning over a relay, but it does impose a round-trip /// cost on the client. #[tokio::test] #[traced_test] async fn relay_retry_then_validated() -> Result { let saw_validated = Arc::::default(); let saw_unvalidated = Arc::::default(); let (sv, su) = (saw_validated.clone(), saw_unvalidated.clone()); let (r1, e2, addr, _guard) = relay_pair(move |incoming| { if incoming.remote_addr_validated() { sv.store(true, Relaxed); IncomingFilterOutcome::Accept } else { su.store(true, Relaxed); IncomingFilterOutcome::Retry } }) .await?; let _conn = e2.connect(addr, ECHO_ALPN).await?; assert!( saw_unvalidated.load(Relaxed), "expected unvalidated incoming" ); assert!( saw_validated.load(Relaxed), "expected validated incoming after retry" ); r1.shutdown().await.anyerr()?; e2.close().await; Ok(()) } } #[tokio::test] async fn test_graceful_shutdown() -> Result { #[derive(Debug, Clone, Default)] struct TestProtocol { connections: Arc>>, } const TEST_ALPN: &[u8] = b"/iroh/test/1"; impl ProtocolHandler for TestProtocol { async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { self.connections.lock().expect("poisoned").push(connection); Ok(()) } async fn shutdown(&self) { tokio::time::sleep(Duration::from_millis(100)).await; let mut connections = self.connections.lock().expect("poisoned"); for conn in connections.drain(..) { conn.close(42u32.into(), b"shutdown"); } } } eprintln!("creating ep1"); let endpoint = Endpoint::bind(presets::Minimal).await?; let router = Router::builder(endpoint) .accept(TEST_ALPN, TestProtocol::default()) .spawn(); eprintln!("waiting for endpoint addr"); let addr = router.endpoint().addr(); eprintln!("creating ep2"); let endpoint2 = Endpoint::bind(presets::Minimal).await?; eprintln!("connecting to {addr:?}"); let conn = endpoint2.connect(addr, TEST_ALPN).await?; eprintln!("starting shutdown"); router.shutdown().await.anyerr()?; eprintln!("waiting for closed conn"); let reason = conn.closed().await; assert_eq!( reason, ConnectionError::ApplicationClosed(ApplicationClose { error_code: 42u32.into(), reason: b"shutdown".to_vec().into() }) ); Ok(()) } }