Skip to main content

iroh_dns_server/
http.rs

1//! HTTP server part of iroh-dns-server
2
3use std::{
4    net::{IpAddr, Ipv4Addr, SocketAddr},
5    path::PathBuf,
6    time::Instant,
7};
8
9use axum::{
10    Json, Router,
11    extract::{ConnectInfo, Request, State},
12    handler::Handler,
13    http::Method,
14    middleware::{self, Next},
15    response::IntoResponse,
16    routing::get,
17};
18use n0_error::{Result, StdResultExt, anyerr, bail_any};
19use serde::{Deserialize, Serialize};
20use tokio::{net::TcpListener, task::JoinSet};
21use tower_http::{
22    cors::{self, CorsLayer},
23    trace::TraceLayer,
24};
25use tracing::{Level, info, span, warn};
26
27mod doh;
28mod error;
29mod pkarr;
30mod rate_limiting;
31mod tls;
32
33pub use self::{rate_limiting::RateLimitConfig, tls::CertMode};
34use crate::state::AppState;
35
36/// Configuration for the HTTP listener.
37#[derive(Debug, Serialize, Deserialize, Clone)]
38#[non_exhaustive]
39pub struct HttpConfig {
40    /// Port to bind the HTTP listener to.
41    pub port: u16,
42    /// Address to bind the HTTP listener to (defaults to `0.0.0.0`).
43    pub bind_addr: Option<IpAddr>,
44}
45
46/// Configuration for the HTTPS listener.
47///
48/// Certificates are obtained according to [`Self::cert_mode`].
49#[derive(Debug, Serialize, Deserialize, Clone)]
50#[non_exhaustive]
51pub struct HttpsConfig {
52    /// Port to bind the HTTPS listener to.
53    pub port: u16,
54    /// Address to bind the HTTPS listener to (defaults to `0.0.0.0`).
55    pub bind_addr: Option<IpAddr>,
56    /// Domains for which TLS certificates are issued or loaded.
57    pub domains: Vec<String>,
58    /// Strategy used to obtain TLS certificates.
59    pub cert_mode: CertMode,
60    /// Contact email address passed to Let's Encrypt.
61    ///
62    /// Required when [`Self::cert_mode`] is [`CertMode::LetsEncrypt`]; ignored
63    /// otherwise.
64    pub letsencrypt_contact: Option<String>,
65    /// Whether to use the Let's Encrypt production endpoint instead of staging.
66    ///
67    /// Only applies when [`Self::cert_mode`] is [`CertMode::LetsEncrypt`]. When
68    /// unset, the ACME staging endpoint is used.
69    pub letsencrypt_prod: Option<bool>,
70}
71
72/// The HTTP(S) server part of iroh-dns-server
73#[derive(Debug)]
74pub(crate) struct HttpServer {
75    tasks: JoinSet<std::io::Result<()>>,
76    http_addr: Option<SocketAddr>,
77    https_addr: Option<SocketAddr>,
78}
79
80impl HttpServer {
81    /// Spawn the server
82    pub(crate) async fn spawn(
83        http_config: Option<HttpConfig>,
84        https_config: Option<HttpsConfig>,
85        rate_limit_config: RateLimitConfig,
86        state: AppState,
87        cert_cache_dir: PathBuf,
88    ) -> Result<HttpServer> {
89        if http_config.is_none() && https_config.is_none() {
90            bail_any!("Either http or https config is required");
91        }
92
93        let app = create_app(state, &rate_limit_config);
94
95        let mut tasks = JoinSet::new();
96
97        // launch http
98        let http_addr = if let Some(config) = http_config {
99            let bind_addr = SocketAddr::new(
100                config.bind_addr.unwrap_or(Ipv4Addr::UNSPECIFIED.into()),
101                config.port,
102            );
103            let app = app.clone();
104            let listener = TcpListener::bind(bind_addr)
105                .await
106                .anyerr()?
107                .into_std()
108                .anyerr()?;
109            let bound_addr = listener.local_addr().anyerr()?;
110            let fut = axum_server::from_tcp(listener)?
111                .serve(app.into_make_service_with_connect_info::<SocketAddr>());
112            info!("HTTP server listening on {bind_addr}");
113            tasks.spawn(fut);
114            Some(bound_addr)
115        } else {
116            None
117        };
118
119        // launch https
120        let https_addr = if let Some(config) = https_config {
121            let bind_addr = SocketAddr::new(
122                config.bind_addr.unwrap_or(Ipv4Addr::UNSPECIFIED.into()),
123                config.port,
124            );
125            let acceptor = {
126                tokio::fs::create_dir_all(&cert_cache_dir)
127                    .await
128                    .with_std_context(|_| {
129                        format!(
130                            "failed to create cert cache dir at {}",
131                            cert_cache_dir.display()
132                        )
133                    })?;
134                config
135                    .cert_mode
136                    .build(
137                        config.domains,
138                        cert_cache_dir,
139                        config.letsencrypt_contact,
140                        config.letsencrypt_prod.unwrap_or(false),
141                    )
142                    .await?
143            };
144            let listener = TcpListener::bind(bind_addr)
145                .await
146                .anyerr()?
147                .into_std()
148                .anyerr()?;
149            let bound_addr = listener.local_addr().anyerr()?;
150            let fut = axum_server::from_tcp(listener)?
151                .acceptor(acceptor)
152                .serve(app.into_make_service_with_connect_info::<SocketAddr>());
153            info!("HTTPS server listening on {bind_addr}");
154            tasks.spawn(fut);
155            Some(bound_addr)
156        } else {
157            None
158        };
159
160        Ok(HttpServer {
161            tasks,
162            http_addr,
163            https_addr,
164        })
165    }
166
167    /// Get the bound address of the HTTP socket.
168    pub(crate) fn http_addr(&self) -> Option<SocketAddr> {
169        self.http_addr
170    }
171
172    /// Get the bound address of the HTTPS socket.
173    pub(crate) fn https_addr(&self) -> Option<SocketAddr> {
174        self.https_addr
175    }
176
177    /// Shutdown the server and wait for all tasks to complete.
178    pub(crate) async fn shutdown(mut self) -> Result<()> {
179        // TODO: Graceful cancellation.
180        self.tasks.abort_all();
181        self.run_until_done().await?;
182        Ok(())
183    }
184
185    /// Wait for all tasks to complete.
186    ///
187    /// Runs forever unless tasks fail.
188    pub(crate) async fn run_until_done(mut self) -> Result<()> {
189        let mut final_res: Result<()> = Ok(());
190        while let Some(res) = self.tasks.join_next().await {
191            match res {
192                Ok(Ok(())) => {}
193                Err(err) if err.is_cancelled() => {}
194                Ok(Err(err)) => {
195                    warn!(?err, "task failed");
196                    final_res = Err(anyerr!(err, "task"));
197                }
198                Err(err) => {
199                    warn!(?err, "task panicked");
200                    final_res = Err(anyerr!(err, "join"));
201                }
202            }
203        }
204        final_res
205    }
206}
207
208/// Health check response
209#[derive(Serialize)]
210struct Health {
211    status: &'static str,
212    version: &'static str,
213    git_hash: &'static str,
214}
215
216async fn healthz() -> Json<Health> {
217    Json(Health {
218        status: "ok",
219        version: env!("CARGO_PKG_VERSION"),
220        git_hash: "unknown",
221    })
222}
223
224pub(crate) fn create_app(state: AppState, rate_limit_config: &RateLimitConfig) -> Router {
225    // configure cors middleware
226    let cors = CorsLayer::new()
227        // allow `GET` and `POST` when accessing the resource
228        .allow_methods([Method::GET, Method::POST, Method::PUT])
229        // allow requests from any origin
230        .allow_origin(cors::Any);
231
232    // configure tracing middleware
233    let trace = TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| {
234        let conn_info = request
235            .extensions()
236            .get::<ConnectInfo<SocketAddr>>()
237            .expect("connectinfo extension to be present");
238        let span = span!(
239        Level::DEBUG,
240            "http_request",
241            method = ?request.method(),
242            uri = ?request.uri(),
243            src = %conn_info.0,
244            );
245        span
246    });
247
248    // configure rate limiting middleware
249    let rate_limit = rate_limiting::create(rate_limit_config);
250
251    // configure routes
252    //
253    // only the pkarr::put route gets a rate limit
254    let router = Router::new()
255        .route("/dns-query", get(doh::get).post(doh::post))
256        .route(
257            "/pkarr/{key}",
258            if let Some(rate_limit) = rate_limit {
259                get(pkarr::get).put(pkarr::put.layer(rate_limit))
260            } else {
261                get(pkarr::get).put(pkarr::put)
262            },
263        )
264        // Deprecated: use /healthz instead
265        .route("/healthcheck", get(|| async { "OK" }))
266        .route("/healthz", get(healthz))
267        .route("/", get(|| async { "Hi!" }))
268        .with_state(state.clone());
269
270    // configure app
271    router
272        .layer(cors)
273        .layer(trace)
274        .route_layer(middleware::from_fn_with_state(state, metrics_middleware))
275}
276
277/// Record request metrics.
278// TODO:
279// * Request duration would be much better tracked as a histogram.
280// * It would be great to attach labels to the metrics, so that the recorded metrics
281// can filter by method etc.
282//
283// See also
284// https://github.com/tokio-rs/axum/blob/main/examples/prometheus-metrics/src/main.rs#L114
285async fn metrics_middleware(
286    State(state): State<AppState>,
287    req: Request,
288    next: Next,
289) -> impl IntoResponse {
290    let start = Instant::now();
291    let response = next.run(req).await;
292    let latency = start.elapsed().as_millis();
293    let status = response.status();
294    state
295        .metrics
296        .http_requests_duration_ms
297        .inc_by(latency as u64);
298    state.metrics.http_requests.inc();
299    if status.is_success() {
300        state.metrics.http_requests_success.inc();
301    } else {
302        state.metrics.http_requests_error.inc();
303    }
304    response
305}
306
307#[cfg(test)]
308mod tests {
309    use std::{
310        net::{IpAddr, Ipv4Addr},
311        sync::Arc,
312    };
313
314    use hickory_resolver::{
315        config::{NameServerConfig, ResolverConfig},
316        net::runtime::TokioRuntimeProvider,
317    };
318    use hickory_server::proto::rr::RecordType;
319    use iroh::{
320        RelayUrl, SecretKey,
321        address_lookup::{EndpointInfo, PkarrRelayClient},
322        dns::DnsResolver,
323        tls::{CaTlsConfig, default_provider},
324    };
325    use n0_error::StdResultExt;
326    use n0_tracing_test::traced_test;
327    use rand::{RngExt, SeedableRng};
328
329    use crate::{http::HttpsConfig, server::Server};
330
331    #[tokio::test]
332    #[traced_test]
333    async fn test_doh() -> n0_error::Result {
334        let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(0);
335        let dir = tempfile::tempdir()?;
336        let https_config = HttpsConfig {
337            port: 0,
338            bind_addr: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
339            domains: vec!["localhost".to_string()],
340            cert_mode: crate::http::CertMode::SelfSigned,
341            letsencrypt_contact: None,
342            letsencrypt_prod: None,
343        };
344        let server =
345            Server::spawn_for_tests_with_options(dir.path(), None, None, Some(https_config))
346                .await?;
347
348        const RELAY_URL: &str = "https://relay.example./";
349        let (name_z32, signed_packet) = {
350            let secret_key = SecretKey::from_bytes(&rng.random());
351            let endpoint_id = secret_key.public();
352            let relay_url: RelayUrl = RELAY_URL.parse().expect("valid url");
353            let endpoint_info = EndpointInfo::new(endpoint_id).with_relay_url(relay_url.clone());
354            (
355                secret_key.public().to_z32(),
356                endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?,
357            )
358        };
359
360        let http_url = server.http_url().expect("http is bound");
361        let tls_config = CaTlsConfig::default()
362            .client_config(default_provider())
363            .expect("infallible");
364        let pkarr = PkarrRelayClient::new(
365            format!("{http_url}pkarr").parse().anyerr()?,
366            tls_config,
367            DnsResolver::default(),
368        );
369        pkarr.publish(&signed_packet).await?;
370
371        // Create a reqwest client that does not verify certificates.
372        let client = reqwest::Client::builder()
373            .http2_prior_knowledge()
374            .use_preconfigured_tls(self::tls::insecure_tls_config())
375            .build()
376            .anyerr()?;
377
378        // Fetch as JSON via HTTP.
379        let url = format!(
380            "{http_url}dns-query?name={}&type=txt",
381            format_args!("_iroh.{name_z32}."),
382        );
383        let res = client
384            .get(url)
385            .header("accept", "application/dns-json")
386            .send()
387            .await
388            .anyerr()?
389            .json::<super::doh::response::DnsResponse>()
390            .await
391            .anyerr()?;
392        assert_eq!(res.answer.len(), 1);
393        assert_eq!(res.answer[0].name, format!("_iroh.{name_z32}."));
394        assert_eq!(res.answer[0].data, format!("relay={RELAY_URL}"));
395
396        // Fetch as JSON via HTTPS.
397        let https_url = server.https_url().expect("https is bound");
398        let url = format!(
399            "{https_url}dns-query?name={}&type=txt",
400            format_args!("_iroh.{name_z32}."),
401        );
402        let res = client
403            .get(url)
404            .header("accept", "application/dns-json")
405            .send()
406            .await
407            .anyerr()?
408            .json::<super::doh::response::DnsResponse>()
409            .await
410            .anyerr()?;
411        assert_eq!(res.answer.len(), 1);
412        assert_eq!(res.answer[0].name, format!("_iroh.{name_z32}."));
413        assert_eq!(res.answer[0].data, format!("relay={RELAY_URL}"));
414
415        // Fetch over HTTPS via hickory-resolver
416        let client = {
417            let https_addr = server.https_addr().expect("https is bound");
418            let mut name_server =
419                NameServerConfig::https(https_addr.ip(), Arc::from("localhost"), None);
420            for connection in &mut name_server.connections {
421                connection.port = https_addr.port();
422            }
423            let config = ResolverConfig::from_parts(None, vec![], vec![name_server]);
424
425            hickory_resolver::Resolver::builder_with_config(config, TokioRuntimeProvider::default())
426                .with_tls_config(self::tls::insecure_tls_config())
427                .build()
428                .anyerr()?
429        };
430
431        let res = client
432            .txt_lookup(format!("_iroh.{name_z32}."))
433            .await
434            .anyerr()?;
435        let records = res.answers();
436        assert_eq!(records.len(), 1);
437        assert_eq!(records[0].record_type(), RecordType::TXT);
438        let txt_data = match &records[0].data {
439            hickory_server::proto::rr::RData::TXT(txt) => &txt.txt_data,
440            other => panic!("expected TXT record, got {other:?}"),
441        };
442        assert_eq!(&txt_data[0][..], format!("relay={RELAY_URL}").as_bytes());
443
444        server.shutdown().await?;
445        Ok(())
446    }
447
448    mod tls {
449        use std::sync::Arc;
450
451        use rustls::{
452            DigitallySignedStruct, RootCertStore,
453            client::{
454                ClientConfig,
455                danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
456            },
457            crypto::{
458                CryptoProvider, ring::default_provider, verify_tls12_signature,
459                verify_tls13_signature,
460            },
461            pki_types::{CertificateDer, ServerName, UnixTime},
462        };
463
464        #[derive(Debug)]
465        struct NoCertificateVerification(CryptoProvider);
466
467        impl Default for NoCertificateVerification {
468            fn default() -> Self {
469                Self(default_provider())
470            }
471        }
472
473        impl ServerCertVerifier for NoCertificateVerification {
474            fn verify_server_cert(
475                &self,
476                _end_entity: &CertificateDer<'_>,
477                _intermediates: &[CertificateDer<'_>],
478                _server_name: &ServerName<'_>,
479                _ocsp: &[u8],
480                _now: UnixTime,
481            ) -> Result<ServerCertVerified, rustls::Error> {
482                Ok(ServerCertVerified::assertion())
483            }
484
485            fn verify_tls12_signature(
486                &self,
487                message: &[u8],
488                cert: &CertificateDer<'_>,
489                dss: &DigitallySignedStruct,
490            ) -> Result<HandshakeSignatureValid, rustls::Error> {
491                verify_tls12_signature(
492                    message,
493                    cert,
494                    dss,
495                    &self.0.signature_verification_algorithms,
496                )
497            }
498
499            fn verify_tls13_signature(
500                &self,
501                message: &[u8],
502                cert: &CertificateDer<'_>,
503                dss: &DigitallySignedStruct,
504            ) -> Result<HandshakeSignatureValid, rustls::Error> {
505                verify_tls13_signature(
506                    message,
507                    cert,
508                    dss,
509                    &self.0.signature_verification_algorithms,
510                )
511            }
512
513            fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
514                self.0.signature_verification_algorithms.supported_schemes()
515            }
516        }
517
518        pub(super) fn insecure_tls_config() -> ClientConfig {
519            let mut cfg = ClientConfig::builder_with_provider(Arc::new(
520                rustls::crypto::ring::default_provider(),
521            ))
522            .with_safe_default_protocol_versions()
523            .unwrap()
524            .with_root_certificates(RootCertStore::empty())
525            .with_no_client_auth();
526            cfg.dangerous()
527                .set_certificate_verifier(Arc::new(NoCertificateVerification::default()));
528            cfg
529        }
530    }
531}