Skip to main content

iroh_dns_server/
http.rs

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