diff --git a/Cargo.lock b/Cargo.lock index 57adb256c06..568bbc177a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,6 +235,32 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-lc-rs" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47bb8cc16b669d267eeccf585aea077d0882f4777b1c1f740217885d6e6e5a3" +dependencies = [ + "aws-lc-sys", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2101df3813227bbaaaa0b04cd61c534c7954b22bd68d399b440be937dc63ff7" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "axum" version = "0.7.9" @@ -379,6 +405,29 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.90", + "which", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -451,6 +500,8 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -460,6 +511,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -534,6 +594,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.21" @@ -574,6 +645,15 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +[[package]] +name = "cmake" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +dependencies = [ + "cc", +] + [[package]] name = "cobs" version = "0.2.3" @@ -1020,6 +1100,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -1269,6 +1355,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -1733,6 +1825,15 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -2405,7 +2506,7 @@ dependencies = [ "iroh-quinn-proto", "iroh-quinn-udp", "pin-project-lite", - "rustc-hash", + "rustc-hash 2.1.0", "rustls", "socket2", "thiserror 1.0.69", @@ -2422,7 +2523,7 @@ dependencies = [ "bytes", "rand", "ring", - "rustc-hash", + "rustc-hash 2.1.0", "rustls", "rustls-platform-verifier", "slab", @@ -2483,9 +2584,12 @@ dependencies = [ "rand_chacha", "rcgen", "regex", + "reloadable-state", "reqwest", "ring", "rustls", + "rustls-cert-file-reader", + "rustls-cert-reloadable-resolver", "rustls-pemfile", "rustls-webpki", "serde", @@ -2583,6 +2687,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.74" @@ -2602,12 +2715,28 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.11" @@ -3595,6 +3724,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.90", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -3728,7 +3867,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.0", "rustls", "socket2", "thiserror 2.0.3", @@ -3746,7 +3885,7 @@ dependencies = [ "getrandom", "rand", "ring", - "rustc-hash", + "rustc-hash 2.1.0", "rustls", "rustls-pki-types", "slab", @@ -3949,6 +4088,23 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reloadable-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc20ac1418988b60072d783c9f68e28a173fb63493c127952f6face3b40c6e0" + +[[package]] +name = "reloadable-state" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3853ef78d45b50f8b989896304a85239539d39b7f866a000e8846b9b72d74ce8" +dependencies = [ + "arc-swap", + "reloadable-core", + "tokio", +] + [[package]] name = "reqwest" version = "0.12.9" @@ -4089,6 +4245,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.0" @@ -4132,6 +4294,7 @@ version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -4141,6 +4304,42 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-cert-file-reader" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07fb1ad76bcc5f2556e38efb848183eea3eb376afe946a5e24d45993e9c8011" +dependencies = [ + "rustls-cert-read", + "rustls-pemfile", + "rustls-pki-types", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rustls-cert-read" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd46e8c5ae4de3345c4786a83f99ec7aff287209b9e26fa883c473aeb28f19d5" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-cert-reloadable-resolver" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c79d0e1801489b80611323abe14b8d8be6e9c4f56a86af73949bb03d1c08481c" +dependencies = [ + "async-trait", + "futures-util", + "reloadable-state", + "rustls", + "rustls-cert-read", + "thiserror 1.0.69", +] + [[package]] name = "rustls-native-certs" version = "0.7.3" @@ -4205,6 +4404,7 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5580,6 +5780,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "widestring" version = "1.1.0" diff --git a/iroh-relay/Cargo.toml b/iroh-relay/Cargo.toml index 1df2cee7ebf..c613d35d616 100644 --- a/iroh-relay/Cargo.toml +++ b/iroh-relay/Cargo.toml @@ -14,6 +14,10 @@ rust-version = "1.76" workspace = true [dependencies] +rustls-cert-reloadable-resolver = "0.6.0" +reloadable-state = "0.1.0" +rustls-cert-file-reader = "0.4.0" + anyhow = { version = "1" } base64 = "0.22.1" bytes = "1.7" diff --git a/iroh-relay/src/main.rs b/iroh-relay/src/main.rs index 73d8825cb36..231f7fec111 100644 --- a/iroh-relay/src/main.rs +++ b/iroh-relay/src/main.rs @@ -6,6 +6,7 @@ use std::{ net::{Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, + sync::Arc, }; use anyhow::{bail, Context as _, Result}; @@ -56,6 +57,7 @@ struct Cli { enum CertMode { Manual, LetsEncrypt, + Reloading, } fn load_certs( @@ -468,6 +470,38 @@ async fn maybe_load_tls( let server_config = server_config.with_cert_resolver(resolver); (relay::CertConfig::LetsEncrypt { state }, server_config) } + CertMode::Reloading => { + use rustls_cert_file_reader::FileReader; + use rustls_cert_reloadable_resolver::{key_provider::Dyn, CertifiedKeyLoader}; + use webpki::types::{CertificateDer, PrivateKeyDer}; + + let cert_path = tls.cert_path(); + let key_path = tls.key_path(); + let interval = std::time::Duration::from_secs(60 * 60 * 24); + + let key_reader = rustls_cert_file_reader::FileReader::new( + key_path, + rustls_cert_file_reader::Format::DER, + ); + let certs_reader = rustls_cert_file_reader::FileReader::new( + cert_path, + rustls_cert_file_reader::Format::DER, + ); + + let loader: CertifiedKeyLoader< + Dyn, + FileReader>, + FileReader>>, + > = CertifiedKeyLoader { + key_provider: Dyn(server_config.crypto_provider().key_provider), + key_reader, + certs_reader, + }; + + let resolver = Arc::new(relay::ReloadingResolver::init(loader, interval).await?); + let server_config = server_config.with_cert_resolver(resolver); + (relay::CertConfig::Reloading, server_config) + } }; Ok(Some(relay::TlsConfig { https_bind_addr: tls.https_bind_addr(cfg), diff --git a/iroh-relay/src/server.rs b/iroh-relay/src/server.rs index a54f4441ceb..185c163d2bf 100644 --- a/iroh-relay/src/server.rs +++ b/iroh-relay/src/server.rs @@ -46,6 +46,7 @@ pub(crate) mod client_conn; mod clients; mod http_server; mod metrics; +pub(crate) mod resolver; pub(crate) mod streams; #[cfg(feature = "test-utils")] #[cfg_attr(iroh_docsrs, doc(cfg(feature = "test-utils")))] @@ -53,6 +54,7 @@ pub mod testing; pub use self::{ metrics::{Metrics, StunMetrics}, + resolver::ReloadingResolver, streams::MaybeTlsStream as MaybeTlsStreamServer, }; @@ -200,6 +202,8 @@ pub enum CertConfig { /// The TLS certificate chain. certs: Vec>, }, + /// Use a TLS key and certificate chain that can be reloaded. + Reloading, } /// A running Relay + STUN server. @@ -281,6 +285,7 @@ impl Server { relay.tls.as_ref().and_then(|tls| match tls.cert { CertConfig::LetsEncrypt { .. } => None, CertConfig::Manual { ref certs, .. } => Some(certs.clone()), + CertConfig::Reloading { .. } => None, }) }); @@ -347,6 +352,16 @@ impl Server { acceptor, }) } + CertConfig::Reloading { .. } => { + let server_config = Arc::new(tls_config.server_config); + let acceptor = + tokio_rustls::TlsAcceptor::from(server_config.clone()); + let acceptor = http_server::TlsAcceptor::Reloading(acceptor); + Some(http_server::TlsConfig { + config: server_config, + acceptor, + }) + } }; builder = builder.tls_config(server_tls_config); diff --git a/iroh-relay/src/server/http_server.rs b/iroh-relay/src/server/http_server.rs index 767f5bb975a..c994af0ad16 100644 --- a/iroh-relay/src/server/http_server.rs +++ b/iroh-relay/src/server/http_server.rs @@ -539,6 +539,8 @@ pub(super) enum TlsAcceptor { /// Manually added tls acceptor. Generally used for tests or for when we've passed in /// a certificate via a file. Manual(#[debug("tokio_rustls::TlsAcceptor")] tokio_rustls::TlsAcceptor), + /// Reloading tls acceptor. This is used when we want to externally manage certificates and live reload them. + Reloading(#[debug("tokio_rustls::TlsAcceptor")] tokio_rustls::TlsAcceptor), } impl RelayService { @@ -611,6 +613,13 @@ impl RelayService { .await .context("TLS[manual] serve connection")?; } + TlsAcceptor::Reloading(a) => { + debug!("TLS[reloading]: accept"); + let tls_stream = a.accept(stream).await.context("TLS[reloading] accept")?; + self.serve_connection(MaybeTlsStream::Tls(tls_stream)) + .await + .context("TLS[reloading] serve connection")?; + } } Ok(()) } diff --git a/iroh-relay/src/server/resolver.rs b/iroh-relay/src/server/resolver.rs new file mode 100644 index 00000000000..b2e53040593 --- /dev/null +++ b/iroh-relay/src/server/resolver.rs @@ -0,0 +1,65 @@ +use std::{sync::Arc, time::Duration}; + +use anyhow::{anyhow, Result}; +use reloadable_state::Reloadable; +use rustls::{ + server::{ClientHello, ResolvesServerCert}, + sign::CertifiedKey, +}; +use tokio::task::JoinHandle; + +/// A Certificate resolver that reloads the certificate every interval +#[derive(Debug)] +pub struct ReloadingResolver { + /// The inner reloadable value. + reloadable: Arc>, + /// The handle to the task that reloads the certificate. + _handle: JoinHandle<()>, +} + +impl ReloadingResolver +where + Loader: Send + reloadable_state::core::Loader + 'static, +{ + /// Perform the initial load and construct the [`ReloadableResolver`]. + pub async fn init(loader: Loader, interval: Duration) -> Result { + let (reloadable, _) = Reloadable::init_load(loader) + .await + .map_err(|_| anyhow!("Failed to load the certificate"))?; + let reloadable = Arc::new(reloadable); + + // Spwan a task to reload the certificate every interval. + let _reloadable = reloadable.clone(); + let _handle = tokio::spawn(async move { + let mut interval = tokio::time::interval(interval); + loop { + interval.tick().await; + let _ = _reloadable.reload().await; + } + }); + + Ok(Self { + reloadable, + _handle, + }) + } +} + +impl ResolvesServerCert for ReloadingResolver +where + Loader: reloadable_state::core::Loader, + Loader: Send, + Loader: std::fmt::Debug, +{ + fn resolve(&self, _client_hello: ClientHello) -> Option> { + Some(self.reloadable.get()) + } +} + +impl std::ops::Deref for ReloadingResolver { + type Target = Reloadable; + + fn deref(&self) -> &Self::Target { + &self.reloadable + } +}