1use std::{
19 borrow::Cow,
20 future::Future,
21 net::SocketAddr,
22 num::NonZeroU32,
23 path::PathBuf,
24 pin::Pin,
25 sync::{
26 Arc,
27 atomic::{AtomicU64, Ordering},
28 },
29 time::Duration,
30};
31
32use derive_more::Debug;
33use http::{
34 HeaderMap, HeaderValue, Method, Request, Response, StatusCode,
35 header::{AUTHORIZATION, InvalidHeaderValue},
36 response::Builder as ResponseBuilder,
37};
38use http_body_util::Full;
39use hyper::body::Incoming;
40use iroh_base::EndpointId;
41#[cfg(feature = "test-utils")]
42use iroh_base::RelayUrl;
43use n0_error::{e, stack_error};
44use n0_future::{StreamExt, task::AbortOnDropHandle};
45use rustls::server::WantsServerCert;
46use serde::Serialize;
47use tokio::{
48 net::TcpListener,
49 task::{JoinError, JoinSet},
50};
51use tokio_rustls_acme::{
52 acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY},
53 caches::DirCache,
54};
55use tracing::{Instrument, debug, error, info, info_span, instrument};
56
57use self::http_server::{BytesBody, HyperError, HyperResult};
58use crate::{
59 defaults::DEFAULT_KEY_CACHE_CAPACITY,
60 http::{AUTH_TOKEN_URL_QUERY_PARAM, ProtocolVersion, RELAY_PROBE_PATH},
61 quic::server::{QuicServer, QuicSpawnError, ServerHandle as QuicServerHandle},
62 tls::CaTlsConfig,
63};
64
65pub mod client;
66pub mod clients;
67pub mod http_server;
68mod metrics;
69pub(crate) mod resolver;
70pub mod streams;
71#[cfg(feature = "test-utils")]
72pub mod testing;
73
74pub use self::{
75 http_server::{Handlers, RelayService},
76 metrics::{Metrics, RelayMetrics},
77 resolver::{DEFAULT_CERT_RELOAD_INTERVAL, reloading_resolver},
78};
79
80const NO_CONTENT_CHALLENGE_HEADER: &str = "X-Iroh-Challenge";
81const NO_CONTENT_RESPONSE_HEADER: &str = "X-Iroh-Response";
82const NOTFOUND: &[u8] = b"Not Found";
83const ROBOTS_TXT: &[u8] = b"User-agent: *\nDisallow: /\n";
84const INDEX: &[u8] = br#"<html><body>
85<h1>Iroh Relay</h1>
86<p>
87 This is an <a href="https://iroh.computer/">Iroh</a> Relay server.
88</p>
89"#;
90const TLS_HEADERS: [(&str, &str); 2] = [
91 (
92 "Strict-Transport-Security",
93 "max-age=63072000; includeSubDomains",
94 ),
95 (
96 "Content-Security-Policy",
97 "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'",
98 ),
99];
100
101fn body_empty() -> BytesBody {
103 Box::new(http_body_util::Full::new(hyper::body::Bytes::new()))
104}
105
106#[derive(Debug, Default)]
111#[non_exhaustive]
112pub struct ServerConfig {
113 pub relay: Option<RelayConfig>,
115 pub quic: Option<QuicConfig>,
117 #[cfg(feature = "metrics")]
119 pub metrics_addr: Option<SocketAddr>,
120}
121
122#[derive(Debug)]
127#[non_exhaustive]
128pub struct RelayConfig {
129 pub http_bind_addr: SocketAddr,
137 pub tls: Option<TlsConfig>,
142 pub limits: Limits,
144 pub key_cache_capacity: Option<usize>,
146 pub access: Arc<dyn DynAccessControl>,
148}
149
150impl RelayConfig {
151 pub fn new(http_bind_addr: impl Into<SocketAddr>) -> Self {
157 Self {
158 http_bind_addr: http_bind_addr.into(),
159 tls: None,
160 limits: Limits::default(),
161 key_cache_capacity: None,
162 access: Arc::new(AllowAll),
163 }
164 }
165}
166
167#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, derive_more::Display)]
175#[display("{_0}")]
176pub struct ConnectionId(u64);
177
178impl ConnectionId {
179 fn next() -> Self {
181 static NEXT: AtomicU64 = AtomicU64::new(0);
182 Self(NEXT.fetch_add(1, Ordering::Relaxed))
183 }
184}
185
186#[derive(Debug, Clone)]
190pub struct ClientRequest {
191 connection_id: ConnectionId,
192 endpoint_id: EndpointId,
193 protocol_version: ProtocolVersion,
194 request: http::request::Parts,
195}
196
197impl ClientRequest {
198 pub fn new(
203 endpoint_id: EndpointId,
204 protocol_version: ProtocolVersion,
205 request: http::request::Parts,
206 ) -> Self {
207 Self {
208 connection_id: ConnectionId::next(),
209 endpoint_id,
210 protocol_version,
211 request,
212 }
213 }
214
215 pub fn connection_id(&self) -> ConnectionId {
217 self.connection_id
218 }
219
220 pub fn protocol_version(&self) -> ProtocolVersion {
222 self.protocol_version
223 }
224
225 pub fn endpoint_id(&self) -> EndpointId {
232 self.endpoint_id
233 }
234
235 pub fn uri(&self) -> &http::Uri {
237 &self.request.uri
238 }
239
240 pub fn query_pairs(&self) -> impl Iterator<Item = (Cow<'_, str>, Cow<'_, str>)> {
245 url::form_urlencoded::parse(self.request.uri.query().unwrap_or("").as_bytes())
246 }
247
248 pub fn headers(&self) -> &http::HeaderMap {
250 &self.request.headers
251 }
252
253 pub fn auth_token(&self) -> Option<String> {
266 for value in self.request.headers.get_all(AUTHORIZATION) {
267 let value = value.to_str().ok()?;
268 if let Some((scheme, token)) = value.split_once(' ')
269 && scheme.eq_ignore_ascii_case("Bearer")
270 {
271 return Some(token.to_string());
272 }
273 }
274 self.query_pairs()
275 .find(|(name, _)| name == AUTH_TOKEN_URL_QUERY_PARAM)
276 .map(|(_, value)| value.into_owned())
277 }
278}
279
280pub trait AccessControl: std::fmt::Debug + Send + Sync + 'static {
287 fn on_connect(&self, request: &ClientRequest) -> impl Future<Output = Access> + Send;
295
296 fn on_disconnect(&self, endpoint_id: EndpointId, connection_id: ConnectionId) {
304 let _ = (endpoint_id, connection_id);
305 }
306}
307
308pub trait DynAccessControl: std::fmt::Debug + Send + Sync + 'static {
314 fn on_connect<'a>(
316 &'a self,
317 request: &'a ClientRequest,
318 ) -> Pin<Box<dyn Future<Output = Access> + Send + 'a>>;
319
320 fn on_disconnect(&self, endpoint_id: EndpointId, connection_id: ConnectionId);
322}
323
324impl<T: AccessControl> DynAccessControl for T {
325 fn on_connect<'a>(
326 &'a self,
327 request: &'a ClientRequest,
328 ) -> Pin<Box<dyn Future<Output = Access> + Send + 'a>> {
329 Box::pin(<Self as AccessControl>::on_connect(self, request))
330 }
331
332 fn on_disconnect(&self, endpoint_id: EndpointId, connection_id: ConnectionId) {
333 <Self as AccessControl>::on_disconnect(self, endpoint_id, connection_id)
334 }
335}
336
337#[derive(Debug, Clone, Copy)]
341pub struct AllowAll;
342
343impl AccessControl for AllowAll {
344 async fn on_connect(&self, _request: &ClientRequest) -> Access {
345 Access::Allow
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
351pub enum Access {
352 Allow,
354 Deny {
356 reason: Option<String>,
358 },
359}
360
361#[derive(Debug)]
377pub struct OnDisconnectGuard {
378 access: Option<Arc<dyn DynAccessControl>>,
379 endpoint_id: EndpointId,
380 connection_id: ConnectionId,
381}
382
383impl OnDisconnectGuard {
384 pub fn for_access_control(access: Arc<dyn DynAccessControl>, request: &ClientRequest) -> Self {
390 Self {
391 access: Some(access),
392 endpoint_id: request.endpoint_id(),
393 connection_id: request.connection_id(),
394 }
395 }
396
397 pub fn empty(endpoint_id: EndpointId) -> Self {
402 Self {
403 access: None,
404 endpoint_id,
405 connection_id: ConnectionId::next(),
406 }
407 }
408
409 pub fn endpoint_id(&self) -> EndpointId {
411 self.endpoint_id
412 }
413
414 pub fn connection_id(&self) -> ConnectionId {
416 self.connection_id
417 }
418}
419
420impl Drop for OnDisconnectGuard {
421 fn drop(&mut self) {
422 if let Some(access) = self.access.as_ref() {
423 access.on_disconnect(self.endpoint_id, self.connection_id);
424 }
425 }
426}
427
428#[derive(Debug)]
430#[non_exhaustive]
431pub struct QuicConfig {
432 pub bind_addr: SocketAddr,
436 pub server_config: Option<rustls::ServerConfig>,
444}
445
446impl QuicConfig {
447 pub fn new(bind_addr: impl Into<SocketAddr>) -> Self {
451 Self {
452 bind_addr: bind_addr.into(),
453 server_config: None,
454 }
455 }
456}
457
458#[derive(Debug)]
462#[non_exhaustive]
463pub struct TlsConfig {
464 pub https_bind_addr: SocketAddr,
472 pub cert: CertConfig,
474}
475
476impl TlsConfig {
477 pub fn new(https_bind_addr: impl Into<SocketAddr>, cert: CertConfig) -> Self {
479 Self {
480 https_bind_addr: https_bind_addr.into(),
481 cert,
482 }
483 }
484}
485
486#[derive(Debug, Default)]
489#[non_exhaustive]
490pub struct Limits {
491 pub client_rx: Option<ClientRateLimit>,
493 pub accept_conn_limit: Option<f64>,
497 pub accept_conn_burst: Option<usize>,
501}
502
503#[derive(Debug, Copy, Clone, PartialEq, Eq)]
505#[non_exhaustive]
506pub struct ClientRateLimit {
507 pub bytes_per_second: NonZeroU32,
509 pub max_burst_bytes: Option<NonZeroU32>,
511}
512
513impl ClientRateLimit {
514 pub fn new(bytes_per_second: NonZeroU32) -> Self {
518 Self {
519 bytes_per_second,
520 max_burst_bytes: None,
521 }
522 }
523}
524
525#[derive(Debug)]
527#[non_exhaustive]
528pub enum CertConfig {
529 LetsEncrypt {
531 acme_config: AcmeConfig,
533 server_config_builder: rustls::ConfigBuilder<rustls::ServerConfig, WantsServerCert>,
537 },
538 Manual {
540 server_config: rustls::ServerConfig,
544 },
545}
546
547#[derive(Debug, Clone)]
549pub struct AcmeConfig {
550 pub(crate) directory_url: String,
551 pub(crate) domains: Vec<String>,
552 pub(crate) contact: Vec<String>,
553 pub(crate) cache_path: Option<PathBuf>,
554 pub(crate) tls_config: CaTlsConfig,
555}
556
557impl AcmeConfig {
558 pub fn new(directory_url: String) -> Self {
560 Self {
561 directory_url,
562 domains: Vec::new(),
563 contact: Vec::new(),
564 cache_path: None,
565 tls_config: CaTlsConfig::default(),
566 }
567 }
568
569 pub fn letsencrypt(production: bool) -> Self {
571 let url = if production {
572 LETS_ENCRYPT_PRODUCTION_DIRECTORY
573 } else {
574 LETS_ENCRYPT_STAGING_DIRECTORY
575 };
576 Self::new(url.to_string())
577 }
578
579 pub fn domains(mut self, domains: Vec<String>) -> Self {
581 self.domains = domains;
582 self
583 }
584
585 pub fn contact(mut self, contact: Vec<String>) -> Self {
589 self.contact = contact;
590 self
591 }
592
593 pub fn cache_path(mut self, path: PathBuf) -> Self {
597 self.cache_path = Some(path);
598 self
599 }
600
601 pub fn tls_config(mut self, tls_config: CaTlsConfig) -> Self {
607 self.tls_config = tls_config;
608 self
609 }
610}
611
612#[derive(Debug)]
618pub struct Server {
619 http_addr: Option<SocketAddr>,
621 https_addr: Option<SocketAddr>,
626 quic_addr: Option<SocketAddr>,
628 relay_handle: Option<http_server::ServerHandle>,
630 relay_service: Option<http_server::RelayService>,
632 quic_handle: Option<QuicServerHandle>,
634 supervisor: AbortOnDropHandle<Result<(), SupervisorError>>,
636 metrics: RelayMetrics,
637 metrics_server: Option<iroh_metrics::service::MetricsServer>,
638}
639
640#[allow(missing_docs)]
642#[stack_error(derive, add_meta, std_sources)]
643#[non_exhaustive]
644pub enum SpawnError {
645 #[error("Unable to get local address")]
646 LocalAddr { source: std::io::Error },
647 #[error("Failed to bind QAD listener")]
648 QuicSpawn { source: QuicSpawnError },
649 #[error("Failed to parse TLS header")]
650 TlsHeaderParse { source: InvalidHeaderValue },
651 #[error("Failed to bind TcpListener")]
652 BindTlsListener { source: std::io::Error },
653 #[error("Failed to build ACME client TLS config")]
654 AcmeClientTlsConfig {
655 #[error(std_err)]
656 source: std::io::Error,
657 },
658 #[error("No local address")]
659 NoLocalAddr { source: std::io::Error },
660 #[error("Failed to bind server socket to {addr}")]
661 BindTcpListener {
662 source: std::io::Error,
663 addr: SocketAddr,
664 },
665 #[error("Error starting metrics server")]
666 Metrics {
667 #[error(std_err)]
668 source: std::io::Error,
669 },
670}
671
672#[allow(missing_docs)]
674#[stack_error(derive, add_meta)]
675#[non_exhaustive]
676pub enum SupervisorError {
677 #[error("Acme event stream finished")]
678 AcmeEventStreamFinished {},
679 #[error(transparent)]
680 JoinError {
681 #[error(from, std_err)]
682 source: JoinError,
683 },
684 #[error("No relay services are enabled")]
685 NoRelayServicesEnabled {},
686 #[error("Task cancelled")]
687 TaskCancelled {},
688}
689
690impl Server {
691 pub async fn spawn(config: ServerConfig) -> Result<Self, SpawnError> {
693 let mut tasks = JoinSet::new();
694
695 let metrics = RelayMetrics::default();
696
697 #[cfg(not(feature = "metrics"))]
698 let metrics_server = None;
699
700 #[cfg(feature = "metrics")]
701 let metrics_server = if let Some(addr) = config.metrics_addr {
702 debug!("Starting metrics server");
703 let mut registry = iroh_metrics::Registry::default();
704 registry.register_all(&metrics);
705 let server = iroh_metrics::service::MetricsServer::spawn(addr, Arc::new(registry))
706 .await
707 .map_err(|err| e!(SpawnError::Metrics, err))?;
708 Some(server)
709 } else {
710 None
711 };
712
713 let (relay_server, http_addr, tls_config) = match config.relay {
714 Some(relay_config) => {
715 debug!("Starting Relay server");
716 let mut headers = HeaderMap::new();
717 for (name, value) in TLS_HEADERS.iter() {
718 headers.insert(
719 *name,
720 value
721 .parse()
722 .map_err(|err| e!(SpawnError::TlsHeaderParse, err))?,
723 );
724 }
725 let relay_bind_addr = match relay_config.tls {
726 Some(ref tls) => tls.https_bind_addr,
727 None => relay_config.http_bind_addr,
728 };
729 let key_cache_capacity = relay_config
730 .key_cache_capacity
731 .unwrap_or(DEFAULT_KEY_CACHE_CAPACITY);
732 let mut builder = http_server::ServerBuilder::new(relay_bind_addr)
733 .metrics(metrics.server.clone())
734 .headers(headers)
735 .key_cache_capacity(key_cache_capacity)
736 .access(relay_config.access)
737 .request_handler(Method::GET, "/", Box::new(root_handler))
738 .request_handler(Method::GET, "/index.html", Box::new(root_handler))
739 .request_handler(Method::GET, RELAY_PROBE_PATH, Box::new(probe_handler))
740 .request_handler(Method::GET, "/robots.txt", Box::new(robots_handler))
741 .request_handler(Method::GET, "/healthz", Box::new(healthz_handler));
742 if let Some(cfg) = relay_config.limits.client_rx {
743 builder = builder.client_rx_ratelimit(cfg);
744 }
745 let (http_addr, tls_config) = match relay_config.tls {
746 Some(tls_config) => {
747 let server_tls_config = match tls_config.cert {
748 CertConfig::LetsEncrypt {
749 acme_config,
750 server_config_builder,
751 } => {
752 let cache = acme_config.cache_path.map(DirCache::new);
753 let crypto_provider =
754 server_config_builder.crypto_provider().clone();
755 let client_tls_config = acme_config
756 .tls_config
757 .client_config(crypto_provider)
758 .map_err(|err| e!(SpawnError::AcmeClientTlsConfig, err))?;
759 let config =
760 tokio_rustls_acme::AcmeConfig::new_with_client_tls_config(
761 acme_config.domains,
762 Arc::new(client_tls_config),
763 )
764 .contact(acme_config.contact)
765 .directory(acme_config.directory_url)
766 .cache_option(cache);
767 let mut state = config.state();
768 let resolver = state.resolver().clone();
769 let server_config =
770 server_config_builder.with_cert_resolver(resolver);
771 let acceptor =
772 http_server::TlsAcceptor::LetsEncrypt(state.acceptor());
773 tasks.spawn(
774 async move {
775 while let Some(event) = state.next().await {
776 match event {
777 Ok(ok) => debug!("acme event: {ok:?}"),
778 Err(err) => error!("error: {err:?}"),
779 }
780 }
781 Err(e!(SupervisorError::AcmeEventStreamFinished))
782 }
783 .instrument(info_span!("acme")),
784 );
785 http_server::TlsConfig {
786 config: Arc::new(server_config),
787 acceptor,
788 }
789 }
790 CertConfig::Manual { server_config } => {
791 let server_config = Arc::new(server_config);
792 let acceptor =
793 tokio_rustls::TlsAcceptor::from(server_config.clone());
794 let acceptor = http_server::TlsAcceptor::Manual(acceptor);
795 http_server::TlsConfig {
796 config: server_config,
797 acceptor,
798 }
799 }
800 };
801 builder = builder.tls_config(Some(server_tls_config.clone()));
802
803 let http_listener = TcpListener::bind(&relay_config.http_bind_addr)
806 .await
807 .map_err(|err| e!(SpawnError::BindTlsListener, err))?;
808 let http_addr = http_listener
809 .local_addr()
810 .map_err(|err| e!(SpawnError::NoLocalAddr, err))?;
811 tasks.spawn(
812 async move {
813 run_captive_portal_service(http_listener).await;
814 Ok(())
815 }
816 .instrument(info_span!("http-service", addr = %http_addr)),
817 );
818 (Some(http_addr), Some(server_tls_config))
819 }
820 None => {
821 builder = builder.request_handler(
824 Method::GET,
825 "/generate_204",
826 Box::new(serve_no_content_handler),
827 );
828 (None, None)
829 }
830 };
831 let relay_server = builder.spawn().await?;
832 (Some(relay_server), http_addr, tls_config)
833 }
834 None => (None, None, None),
835 };
836 let relay_addr = relay_server.as_ref().map(|srv| srv.addr());
839 let relay_handle = relay_server.as_ref().map(|srv| srv.handle());
840 let relay_service = relay_server.as_ref().map(|srv| srv.service().clone());
841
842 let quic_server = match config.quic {
843 Some(quic_config) => {
844 debug!("Starting QUIC server {}", quic_config.bind_addr);
845 let server_config = quic_config
846 .server_config
847 .or(tls_config.map(|config| (*config.config).clone()))
848 .ok_or_else(|| {
849 e!(SpawnError::QuicSpawn, e!(QuicSpawnError::TlsNotConfigured))
850 })?;
851 Some(
852 QuicServer::spawn(quic_config.bind_addr, server_config, metrics.server.clone())
853 .map_err(|err| e!(SpawnError::QuicSpawn, err))?,
854 )
855 }
856 None => None,
857 };
858 let quic_addr = quic_server.as_ref().map(|srv| srv.bind_addr());
859 let quic_handle = quic_server.as_ref().map(|srv| srv.handle());
860
861 let task = tokio::spawn(relay_supervisor(tasks, relay_server, quic_server));
862
863 Ok(Self {
864 http_addr: http_addr.or(relay_addr),
865 https_addr: http_addr.and(relay_addr),
866 quic_addr,
867 relay_handle,
868 relay_service,
869 quic_handle,
870 supervisor: AbortOnDropHandle::new(task),
871 metrics,
872 metrics_server,
873 })
874 }
875
876 pub async fn shutdown(self) -> Result<(), SupervisorError> {
880 if let Some(handle) = self.relay_handle {
883 handle.shutdown();
884 }
885 if let Some(handle) = self.quic_handle {
886 handle.shutdown();
887 }
888 if let Some(server) = self.metrics_server {
889 server.shutdown().await;
890 }
891 self.supervisor.await?
892 }
893
894 pub async fn join(&mut self) -> Result<Result<(), SupervisorError>, JoinError> {
902 (&mut self.supervisor).await
903 }
904
905 pub fn https_addr(&self) -> Option<SocketAddr> {
907 self.https_addr
908 }
909
910 pub fn http_addr(&self) -> Option<SocketAddr> {
912 self.http_addr
913 }
914
915 pub fn quic_addr(&self) -> Option<SocketAddr> {
917 self.quic_addr
918 }
919
920 #[cfg(feature = "test-utils")]
924 pub fn https_url(&self) -> Option<RelayUrl> {
925 self.https_addr.map(|addr| {
926 url::Url::parse(&format!("https://{addr}"))
927 .expect("valid url")
928 .into()
929 })
930 }
931
932 #[cfg(feature = "test-utils")]
936 pub fn http_url(&self) -> Option<RelayUrl> {
937 self.http_addr.map(|addr| {
938 url::Url::parse(&format!("http://{addr}"))
939 .expect("valid url")
940 .into()
941 })
942 }
943
944 pub fn metrics(&self) -> &RelayMetrics {
946 &self.metrics
947 }
948
949 pub fn relay_service(&self) -> Option<&RelayService> {
951 self.relay_service.as_ref()
952 }
953}
954
955#[instrument(skip_all)]
960async fn relay_supervisor(
961 mut tasks: JoinSet<Result<(), SupervisorError>>,
962 mut relay_http_server: Option<http_server::Server>,
963 mut quic_server: Option<QuicServer>,
964) -> Result<(), SupervisorError> {
965 let quic_enabled = quic_server.is_some();
966 let mut quic_fut = match quic_server {
967 Some(ref mut server) => n0_future::Either::Left(server.task_handle()),
968 None => n0_future::Either::Right(n0_future::future::pending()),
969 };
970 let relay_enabled = relay_http_server.is_some();
971 let mut relay_fut = match relay_http_server {
972 Some(ref mut server) => n0_future::Either::Left(server.task_handle()),
973 None => n0_future::Either::Right(n0_future::future::pending()),
974 };
975 let res = tokio::select! {
976 biased;
977 Some(ret) = tasks.join_next() => ret,
978 ret = &mut quic_fut, if quic_enabled => ret.map(Ok),
979 ret = &mut relay_fut, if relay_enabled => ret.map(Ok),
980 else => Ok(Err(e!(SupervisorError::NoRelayServicesEnabled))),
981 };
982 let ret = match res {
983 Ok(Ok(())) => {
984 debug!("Task exited");
985 Ok(())
986 }
987 Ok(Err(err)) => {
988 error!(%err, "Task failed");
989 Err(err)
990 }
991 Err(err) => {
992 if let Ok(panic) = err.try_into_panic() {
993 error!("Task panicked");
994 std::panic::resume_unwind(panic);
995 }
996 debug!("Task cancelled");
997 Err(e!(SupervisorError::TaskCancelled))
998 }
999 };
1000
1001 if let Some(server) = relay_http_server {
1004 server.shutdown();
1005 }
1006
1007 if let Some(server) = quic_server {
1009 server.shutdown().await;
1010 }
1011
1012 tasks.shutdown().await;
1014
1015 ret
1016}
1017
1018fn root_handler(
1019 _r: Request<Incoming>,
1020 response: ResponseBuilder,
1021) -> HyperResult<Response<BytesBody>> {
1022 let body: BytesBody = Box::new(Full::from(INDEX));
1023 response
1024 .status(StatusCode::OK)
1025 .header("Content-Type", "text/html; charset=utf-8")
1026 .body(body)
1027 .map_err(|err| Box::new(err) as HyperError)
1028}
1029
1030fn probe_handler(
1032 _r: Request<Incoming>,
1033 response: ResponseBuilder,
1034) -> HyperResult<Response<BytesBody>> {
1035 response
1036 .status(StatusCode::OK)
1037 .header("Access-Control-Allow-Origin", "*")
1038 .body(body_empty())
1039 .map_err(|err| Box::new(err) as HyperError)
1040}
1041
1042fn robots_handler(
1043 _r: Request<Incoming>,
1044 response: ResponseBuilder,
1045) -> HyperResult<Response<BytesBody>> {
1046 let body: BytesBody = Box::new(Full::from(ROBOTS_TXT));
1047 response
1048 .status(StatusCode::OK)
1049 .body(body)
1050 .map_err(|err| Box::new(err) as HyperError)
1051}
1052
1053fn serve_no_content_handler<B: hyper::body::Body>(
1055 r: Request<B>,
1056 mut response: ResponseBuilder,
1057) -> HyperResult<Response<BytesBody>> {
1058 let check = |c: &HeaderValue| {
1059 !c.is_empty() && c.len() < 64 && c.as_bytes().iter().all(|c| is_challenge_char(*c as char))
1060 };
1061
1062 if let Some(challenge) = r.headers().get(NO_CONTENT_CHALLENGE_HEADER)
1063 && check(challenge)
1064 {
1065 response = response.header(
1066 NO_CONTENT_RESPONSE_HEADER,
1067 format!("response {}", challenge.to_str()?),
1068 );
1069 }
1070
1071 response
1072 .status(StatusCode::NO_CONTENT)
1073 .body(body_empty())
1074 .map_err(|err| Box::new(err) as HyperError)
1075}
1076
1077fn is_challenge_char(c: char) -> bool {
1078 c.is_ascii_lowercase()
1080 || c.is_ascii_uppercase()
1081 || c.is_ascii_digit()
1082 || c == '.'
1083 || c == '-'
1084 || c == '_'
1085}
1086
1087#[derive(Serialize)]
1089struct Health {
1090 status: &'static str,
1091 version: &'static str,
1092 git_hash: &'static str,
1093}
1094
1095fn healthz_handler(
1096 _r: Request<Incoming>,
1097 response: ResponseBuilder,
1098) -> HyperResult<Response<BytesBody>> {
1099 let health = Health {
1100 status: "ok",
1101 version: env!("CARGO_PKG_VERSION"),
1102 git_hash: "unknown",
1103 };
1104 let body = serde_json::to_string(&health).unwrap_or_else(|_| r#"{"status":"error"}"#.into());
1105 let body: BytesBody = Box::new(Full::from(body));
1106 response
1107 .status(StatusCode::OK)
1108 .header("Content-Type", "application/json")
1109 .body(body)
1110 .map_err(|err| Box::new(err) as HyperError)
1111}
1112
1113async fn run_captive_portal_service(http_listener: TcpListener) {
1115 info!("serving");
1116
1117 let mut tasks = JoinSet::new();
1119
1120 loop {
1121 tokio::select! {
1122 biased;
1123
1124 Some(res) = tasks.join_next() => {
1125 if let Err(err) = res
1126 && err.is_panic()
1127 {
1128 panic!("task panicked: {err:#?}");
1129 }
1130 }
1131
1132 res = http_listener.accept() => {
1133 match res {
1134 Ok((stream, peer_addr)) => {
1135 debug!(%peer_addr, "Connection opened",);
1136 let handler = CaptivePortalService;
1137
1138 let stream = crate::server::streams::MaybeTlsStream::Plain(stream);
1139 let stream = hyper_util::rt::TokioIo::new(stream);
1140 let connection = hyper::server::conn::http1::Builder::new()
1141 .keep_alive(false)
1142 .serve_connection(stream, handler);
1143 tasks.spawn(async move {
1144 match tokio::time::timeout(Duration::from_secs(30), connection).await {
1145 Ok(Ok(())) => {}
1146 Ok(Err(err)) => error!("Failed to serve connection: {err:?}"),
1147 Err(_) => debug!(%peer_addr, "Captive portal connection timed out"),
1148 }
1149 });
1150 }
1151 Err(err) => {
1152 error!(
1153 "[CaptivePortalService] failed to accept connection: {:#?}",
1154 err
1155 );
1156 }
1157 }
1158 }
1159 }
1160 }
1161}
1162
1163#[derive(Clone)]
1164struct CaptivePortalService;
1165
1166impl hyper::service::Service<Request<Incoming>> for CaptivePortalService {
1167 type Response = Response<BytesBody>;
1168 type Error = HyperError;
1169 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
1170
1171 fn call(&self, req: Request<Incoming>) -> Self::Future {
1172 match (req.method(), req.uri().path()) {
1173 (&Method::GET, "/generate_204") => {
1175 Box::pin(async move { serve_no_content_handler(req, Response::builder()) })
1176 }
1177 _ => {
1178 let body: BytesBody = Box::new(Full::from(NOTFOUND));
1180 let r = Response::builder()
1181 .status(StatusCode::NOT_FOUND)
1182 .body(body)
1183 .map_err(|err| Box::new(err) as HyperError);
1184 Box::pin(async move { r })
1185 }
1186 }
1187 }
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192 use std::{net::Ipv4Addr, sync::Arc, time::Duration};
1193
1194 use http::StatusCode;
1195 use iroh_base::{EndpointId, RelayUrl, SecretKey};
1196 use iroh_dns::dns::DnsResolver;
1197 use n0_error::{Result, StackResultExt, StdResultExt};
1198 use n0_future::{SinkExt, StreamExt};
1199 use n0_tracing_test::traced_test;
1200 use rand::{RngExt, SeedableRng};
1201 use tracing::{info, instrument};
1202 use url::Url;
1203
1204 use super::{
1205 Access, AccessControl, ClientRequest, NO_CONTENT_CHALLENGE_HEADER,
1206 NO_CONTENT_RESPONSE_HEADER, RelayConfig, Server, ServerConfig, SpawnError,
1207 };
1208 use crate::{
1209 client::{ClientBuilder, ConnectError},
1210 protos::{
1211 handshake,
1212 relay::{ClientToRelayMsg, Datagrams, RelayToClientMsg},
1213 },
1214 test_utils::static_resolver,
1215 tls::{self, CaTlsConfig, default_provider},
1216 };
1217
1218 #[derive(derive_more::Debug)]
1220 struct TestAccess(#[debug("access fn")] Box<dyn Fn(&ClientRequest) -> Access + Send + Sync>);
1221
1222 impl AccessControl for TestAccess {
1223 async fn on_connect(&self, request: &ClientRequest) -> Access {
1224 (self.0)(request)
1225 }
1226 }
1227
1228 async fn spawn_local_relay() -> std::result::Result<Server, SpawnError> {
1229 let mut relay = RelayConfig::new((Ipv4Addr::LOCALHOST, 0));
1230 relay.key_cache_capacity = Some(1024);
1231 Server::spawn(ServerConfig {
1232 relay: Some(relay),
1233 quic: None,
1234 metrics_addr: None,
1235 })
1236 .await
1237 }
1238
1239 #[instrument]
1240 async fn try_send_recv(
1241 client_a: &mut crate::client::Client,
1242 client_b: &mut crate::client::Client,
1243 b_key: EndpointId,
1244 msg: Datagrams,
1245 ) -> Result<RelayToClientMsg> {
1246 for _ in 0..10 {
1248 client_a
1249 .send(ClientToRelayMsg::Datagrams {
1250 dst_endpoint_id: b_key,
1251 datagrams: msg.clone(),
1252 })
1253 .await?;
1254 let Ok(res) = tokio::time::timeout(Duration::from_millis(500), client_b.next()).await
1255 else {
1256 continue;
1257 };
1258 let res = res.expect("stream finished")?;
1259 return Ok(res);
1260 }
1261 panic!("failed to send and recv message");
1262 }
1263
1264 fn dns_resolver() -> DnsResolver {
1265 DnsResolver::new()
1266 }
1267
1268 fn ring_config() -> rustls::ClientConfig {
1269 rustls::ClientConfig::builder_with_provider(Arc::new(
1270 rustls::crypto::ring::default_provider(),
1271 ))
1272 .with_safe_default_protocol_versions()
1273 .unwrap()
1274 .with_root_certificates(rustls::RootCertStore::empty())
1275 .with_no_client_auth()
1276 }
1277
1278 #[tokio::test]
1279 #[traced_test]
1280 async fn test_no_services() {
1281 let mut server = Server::spawn(ServerConfig::default()).await.unwrap();
1282 let res = tokio::time::timeout(Duration::from_secs(5), server.join())
1283 .await
1284 .expect("timeout, server not finished")
1285 .expect("server task JoinError");
1286 assert!(res.is_err());
1287 }
1288
1289 #[tokio::test]
1290 #[traced_test]
1291 async fn test_conflicting_bind() {
1292 let mut relay = RelayConfig::new((Ipv4Addr::LOCALHOST, 1234));
1293 relay.key_cache_capacity = Some(1024);
1294 let res = Server::spawn(ServerConfig {
1295 relay: Some(relay),
1296 quic: None,
1297 metrics_addr: Some((Ipv4Addr::LOCALHOST, 1234).into()),
1298 })
1299 .await;
1300 assert!(res.is_err()); }
1302
1303 #[tokio::test]
1304 #[traced_test]
1305 async fn test_root_handler() {
1306 let server = spawn_local_relay().await.unwrap();
1307 let url = format!("http://{}", server.http_addr().unwrap());
1308
1309 let client = reqwest::Client::builder()
1310 .use_preconfigured_tls(ring_config())
1311 .build()
1312 .unwrap();
1313 let response = client.get(&url).send().await.unwrap();
1314 assert_eq!(response.status(), 200);
1315 let body = response.text().await.unwrap();
1316 assert!(body.contains("iroh.computer"));
1317 }
1318
1319 #[tokio::test]
1320 #[traced_test]
1321 async fn test_captive_portal_service() {
1322 let server = spawn_local_relay().await.unwrap();
1323 let url = format!("http://{}/generate_204", server.http_addr().unwrap());
1324 let challenge = "123az__.";
1325
1326 let client = reqwest::Client::builder()
1327 .use_preconfigured_tls(ring_config())
1328 .build()
1329 .unwrap();
1330 let response = client
1331 .get(&url)
1332 .header(NO_CONTENT_CHALLENGE_HEADER, challenge)
1333 .send()
1334 .await
1335 .unwrap();
1336 assert_eq!(response.status(), StatusCode::NO_CONTENT);
1337 let header = response.headers().get(NO_CONTENT_RESPONSE_HEADER).unwrap();
1338 assert_eq!(header.to_str().unwrap(), format!("response {challenge}"));
1339 let body = response.text().await.unwrap();
1340 assert!(body.is_empty());
1341 }
1342
1343 #[tokio::test]
1344 #[traced_test]
1345 async fn test_relay_clients() -> Result<()> {
1346 let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
1347 let server = spawn_local_relay().await?;
1348
1349 let relay_url = format!("http://{}", server.http_addr().unwrap());
1350 let relay_url: RelayUrl = relay_url.parse()?;
1351
1352 let client_config = CaTlsConfig::default()
1353 .client_config(default_provider())
1354 .unwrap();
1355
1356 let a_secret_key = SecretKey::from_bytes(&rng.random());
1358 let a_key = a_secret_key.public();
1359 let resolver = dns_resolver();
1360 info!("client a build & connect");
1361 let mut client_a = ClientBuilder::new(relay_url.clone(), a_secret_key, resolver.clone())
1362 .tls_client_config(client_config.clone())
1363 .connect()
1364 .await?;
1365
1366 let b_secret_key = SecretKey::from_bytes(&rng.random());
1368 let b_key = b_secret_key.public();
1369 info!("client b build & connect");
1370 let mut client_b = ClientBuilder::new(relay_url.clone(), b_secret_key, resolver.clone())
1371 .tls_client_config(client_config)
1372 .connect()
1373 .await?;
1374
1375 info!("sending a -> b");
1376
1377 let msg = Datagrams::from("hello, b");
1379 let res = try_send_recv(&mut client_a, &mut client_b, b_key, msg.clone()).await?;
1380 let RelayToClientMsg::Datagrams {
1381 remote_endpoint_id,
1382 datagrams,
1383 } = res
1384 else {
1385 panic!("client_b received unexpected message {res:?}");
1386 };
1387
1388 assert_eq!(a_key, remote_endpoint_id);
1389 assert_eq!(msg, datagrams);
1390
1391 info!("sending b -> a");
1392 let msg = Datagrams::from("howdy, a");
1394 let res = try_send_recv(&mut client_b, &mut client_a, a_key, msg.clone()).await?;
1395
1396 let RelayToClientMsg::Datagrams {
1397 remote_endpoint_id,
1398 datagrams,
1399 } = res
1400 else {
1401 panic!("client_a received unexpected message {res:?}");
1402 };
1403
1404 assert_eq!(b_key, remote_endpoint_id);
1405 assert_eq!(msg, datagrams);
1406
1407 Ok(())
1408 }
1409
1410 #[tokio::test]
1411 #[traced_test]
1412 async fn test_relay_access_control() -> Result<()> {
1413 let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
1414 let current_span = tracing::info_span!("this is a test");
1415 let _guard = current_span.enter();
1416
1417 let client_config = CaTlsConfig::default()
1418 .client_config(default_provider())
1419 .unwrap();
1420
1421 let a_secret_key = SecretKey::from_bytes(&rng.random());
1422 let a_key = a_secret_key.public();
1423
1424 let mut relay = RelayConfig::new((Ipv4Addr::LOCALHOST, 0));
1425 relay.key_cache_capacity = Some(1024);
1426 relay.access = Arc::new(TestAccess(Box::new(move |request| {
1427 let endpoint_id = request.endpoint_id();
1428 info!("checking {}", endpoint_id);
1429 if endpoint_id == a_key {
1431 Access::Deny { reason: None }
1432 } else {
1433 Access::Allow
1434 }
1435 })));
1436 let server = Server::spawn(ServerConfig {
1437 relay: Some(relay),
1438 quic: None,
1439 metrics_addr: None,
1440 })
1441 .await?;
1442
1443 let relay_url = format!("http://{}", server.http_addr().unwrap());
1444 let relay_url: RelayUrl = relay_url.parse()?;
1445
1446 let resolver = dns_resolver();
1448 let result = ClientBuilder::new(relay_url.clone(), a_secret_key, resolver)
1449 .tls_client_config(client_config.clone())
1450 .connect()
1451 .await;
1452
1453 assert!(
1454 matches!(result, Err(ConnectError::Handshake { source: handshake::Error::ServerDeniedAuth { reason, .. }, .. }) if reason == "not authorized")
1455 );
1456
1457 let b_secret_key = SecretKey::from_bytes(&rng.random());
1461 let b_key = b_secret_key.public();
1462
1463 let resolver = dns_resolver();
1464 let mut client_b = ClientBuilder::new(relay_url.clone(), b_secret_key, resolver)
1465 .tls_client_config(client_config.clone())
1466 .connect()
1467 .await?;
1468
1469 let c_secret_key = SecretKey::from_bytes(&rng.random());
1471 let c_key = c_secret_key.public();
1472
1473 let resolver = dns_resolver();
1474 let mut client_c = ClientBuilder::new(relay_url.clone(), c_secret_key, resolver)
1475 .tls_client_config(client_config)
1476 .connect()
1477 .await?;
1478
1479 let msg = Datagrams::from("hello, c");
1481 let res = try_send_recv(&mut client_b, &mut client_c, c_key, msg.clone()).await?;
1482
1483 if let RelayToClientMsg::Datagrams {
1484 remote_endpoint_id,
1485 datagrams,
1486 } = res
1487 {
1488 assert_eq!(b_key, remote_endpoint_id);
1489 assert_eq!(msg, datagrams);
1490 } else {
1491 panic!("client_c received unexpected message {res:?}");
1492 }
1493
1494 Ok(())
1495 }
1496
1497 #[tokio::test]
1501 #[traced_test]
1502 async fn test_relay_client_auth_token_forwarded() -> Result<()> {
1503 const TOKEN: &str = "secret-token";
1504
1505 let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
1506 let client_config = CaTlsConfig::default()
1507 .client_config(default_provider())
1508 .unwrap();
1509
1510 let mut relay = RelayConfig::new((Ipv4Addr::LOCALHOST, 0));
1511 relay.key_cache_capacity = Some(1024);
1512 relay.access = Arc::new(TestAccess(Box::new(move |request| {
1513 if request.auth_token().as_deref() == Some(TOKEN) {
1514 Access::Allow
1515 } else {
1516 Access::Deny { reason: None }
1517 }
1518 })));
1519 let server = Server::spawn(ServerConfig {
1520 relay: Some(relay),
1521 quic: None,
1522 metrics_addr: None,
1523 })
1524 .await?;
1525
1526 let relay_url = format!("http://{}", server.http_addr().unwrap());
1527 let relay_url: RelayUrl = relay_url.parse()?;
1528
1529 let secret_key = SecretKey::from_bytes(&rng.random());
1531 let result = ClientBuilder::new(relay_url.clone(), secret_key, dns_resolver())
1532 .tls_client_config(client_config.clone())
1533 .connect()
1534 .await;
1535 assert!(matches!(
1536 result,
1537 Err(ConnectError::Handshake { source: handshake::Error::ServerDeniedAuth { reason, .. }, .. })
1538 if reason == "not authorized"
1539 ));
1540
1541 let secret_key = SecretKey::from_bytes(&rng.random());
1543 let result = ClientBuilder::new(relay_url.clone(), secret_key, dns_resolver())
1544 .tls_client_config(client_config.clone())
1545 .auth_token("wrong-token")
1546 .connect()
1547 .await;
1548 assert!(matches!(
1549 result,
1550 Err(ConnectError::Handshake { source: handshake::Error::ServerDeniedAuth { reason, .. }, .. })
1551 if reason == "not authorized"
1552 ));
1553
1554 let secret_key = SecretKey::from_bytes(&rng.random());
1556 let _client = ClientBuilder::new(relay_url, secret_key, dns_resolver())
1557 .tls_client_config(client_config)
1558 .auth_token(TOKEN)
1559 .connect()
1560 .await?;
1561
1562 Ok(())
1563 }
1564
1565 #[tokio::test]
1566 #[traced_test]
1567 async fn test_relay_clients_full() -> Result<()> {
1568 let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
1569 let server = spawn_local_relay().await.unwrap();
1570 let relay_url = format!("http://{}", server.http_addr().unwrap());
1571 let relay_url: RelayUrl = relay_url.parse().unwrap();
1572
1573 let client_config = CaTlsConfig::default()
1574 .client_config(default_provider())
1575 .unwrap();
1576
1577 let a_secret_key = SecretKey::from_bytes(&rng.random());
1579 let resolver = dns_resolver();
1580 let mut client_a = ClientBuilder::new(relay_url.clone(), a_secret_key, resolver.clone())
1581 .tls_client_config(client_config.clone())
1582 .connect()
1583 .await?;
1584
1585 let b_secret_key = SecretKey::from_bytes(&rng.random());
1587 let b_key = b_secret_key.public();
1588 let _client_b = ClientBuilder::new(relay_url.clone(), b_secret_key, resolver.clone())
1589 .tls_client_config(client_config)
1590 .connect()
1591 .await?;
1592
1593 let msg = Datagrams::from("hello, b");
1597 for _i in 0..1000 {
1598 client_a
1599 .send(ClientToRelayMsg::Datagrams {
1600 dst_endpoint_id: b_key,
1601 datagrams: msg.clone(),
1602 })
1603 .await?;
1604 }
1605 Ok(())
1606 }
1607
1608 #[tokio::test]
1611 #[traced_test]
1612 async fn test_relay_client_falls_back_to_ipv4() -> Result {
1613 let config = ServerConfig {
1615 relay: Some(RelayConfig::new((Ipv4Addr::LOCALHOST, 0))),
1616 ..Default::default()
1617 };
1618 let server = Server::spawn(config).await?;
1619 let addr = server.http_addr().expect("http relay address");
1620
1621 let resolver = static_resolver(
1623 vec![Ipv4Addr::LOCALHOST],
1624 vec!["2001:db8::dead".parse().expect("valid IPv6")],
1625 );
1626 let url: Url = format!("http://relay.test:{}", addr.port())
1627 .parse()
1628 .expect("valid relay url");
1629
1630 let client = ClientBuilder::new(url, SecretKey::generate(), resolver)
1631 .tls_client_config(tls::make_dangerous_client_config())
1632 .address_family_selector(|| true);
1634
1635 tokio::time::timeout(Duration::from_secs(10), client.connect())
1636 .await
1637 .with_std_context(|_| "relay connect timed out")?
1638 .context("relay connect")?;
1639
1640 server.shutdown().await.context("relay server shutdown")?;
1641 Ok(())
1642 }
1643}