From 887e6b64b0f1984fcfdf3691c8b76ef74f002d27 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 12 Jul 2024 23:50:37 +0900 Subject: [PATCH] feat: wip - configuration design --- Cargo.toml | 2 +- rpxy-acme/Cargo.toml | 17 +++++++++ rpxy-acme/src/constants.rs | 14 ++++++++ rpxy-acme/src/error.rs | 15 ++++++++ rpxy-acme/src/lib.rs | 12 +++++++ rpxy-bin/Cargo.toml | 7 ++-- rpxy-bin/src/config/mod.rs | 5 ++- rpxy-bin/src/config/parse.rs | 58 +++++++++++++++++++++++++++++- rpxy-bin/src/config/toml.rs | 28 +++++++++++++++ rpxy-bin/src/main.rs | 8 +++++ rpxy-certs/src/reloader_service.rs | 11 +++--- rpxy-lib/Cargo.toml | 5 +-- rpxy-lib/src/globals.rs | 2 ++ 13 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 rpxy-acme/Cargo.toml create mode 100644 rpxy-acme/src/constants.rs create mode 100644 rpxy-acme/src/error.rs create mode 100644 rpxy-acme/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 355e5b1..68be1fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" publish = false [workspace] -members = ["rpxy-bin", "rpxy-lib", "rpxy-certs"] +members = ["rpxy-bin", "rpxy-lib", "rpxy-certs", "rpxy-acme"] exclude = ["submodules"] resolver = "2" diff --git a/rpxy-acme/Cargo.toml b/rpxy-acme/Cargo.toml new file mode 100644 index 0000000..4ceeaf3 --- /dev/null +++ b/rpxy-acme/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rpxy-acme" +description = "ACME manager library for `rpxy`" +version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +url = { version = "2.5.2" } +rustc-hash = "2.0.0" +thiserror = "1.0.62" +tracing = "0.1.40" diff --git a/rpxy-acme/src/constants.rs b/rpxy-acme/src/constants.rs new file mode 100644 index 0000000..edb657b --- /dev/null +++ b/rpxy-acme/src/constants.rs @@ -0,0 +1,14 @@ +/// ACME directory url +pub const ACME_DIR_URL: &str = "https://acme-v02.api.letsencrypt.org/directory"; + +/// ACME registry path that stores account key and certificate +pub const ACME_REGISTRY_PATH: &str = "./acme_registry"; + +/// ACME accounts directory, subdirectory of ACME_REGISTRY_PATH +pub(crate) const ACME_ACCOUNT_SUBDIR: &str = "account"; + +/// ACME private key file name +pub const ACME_PRIVATE_KEY_FILE_NAME: &str = "private_key.pem"; + +/// ACME certificate file name +pub const ACME_CERTIFICATE_FILE_NAME: &str = "certificate.pem"; diff --git a/rpxy-acme/src/error.rs b/rpxy-acme/src/error.rs new file mode 100644 index 0000000..08133c5 --- /dev/null +++ b/rpxy-acme/src/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +/// Error type for rpxy-acme +pub enum RpxyAcmeError { + /// Invalid acme registry path + #[error("Invalid acme registry path")] + InvalidAcmeRegistryPath, + /// Invalid url + #[error("Invalid url: {0}")] + InvalidUrl(#[from] url::ParseError), + /// IO error + #[error("IO error: {0}")] + Io(#[from] std::io::Error), +} diff --git a/rpxy-acme/src/lib.rs b/rpxy-acme/src/lib.rs new file mode 100644 index 0000000..1fec84c --- /dev/null +++ b/rpxy-acme/src/lib.rs @@ -0,0 +1,12 @@ +mod constants; +mod error; +mod targets; + +#[allow(unused_imports)] +mod log { + pub(super) use tracing::{debug, error, info, warn}; +} + +pub use constants::{ACME_CERTIFICATE_FILE_NAME, ACME_DIR_URL, ACME_PRIVATE_KEY_FILE_NAME, ACME_REGISTRY_PATH}; +pub use error::RpxyAcmeError; +pub use targets::AcmeTargets; diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index d571d1e..358ee43 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -13,14 +13,15 @@ publish.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["http3-quinn", "cache", "rustls-backend"] -# default = ["http3-s2n", "cache", "rustls-backend"] +default = ["http3-quinn", "cache", "rustls-backend", "acme"] +# default = ["http3-s2n", "cache", "rustls-backend", "acme"] http3-quinn = ["rpxy-lib/http3-quinn"] http3-s2n = ["rpxy-lib/http3-s2n"] native-tls-backend = ["rpxy-lib/native-tls-backend"] rustls-backend = ["rpxy-lib/rustls-backend"] webpki-roots = ["rpxy-lib/webpki-roots"] cache = ["rpxy-lib/cache"] +acme = ["rpxy-lib/acme", "rpxy-acme"] [dependencies] rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ @@ -56,4 +57,6 @@ rpxy-certs = { path = "../rpxy-certs/", default-features = false, features = [ "http3", ] } +rpxy-acme = { path = "../rpxy-acme/", default-features = false, optional = true } + [dev-dependencies] diff --git a/rpxy-bin/src/config/mod.rs b/rpxy-bin/src/config/mod.rs index adc4ff2..079e477 100644 --- a/rpxy-bin/src/config/mod.rs +++ b/rpxy-bin/src/config/mod.rs @@ -3,7 +3,10 @@ mod service; mod toml; pub use { - self::toml::ConfigToml, parse::{build_cert_manager, build_settings, parse_opts}, service::ConfigTomlReloader, + toml::ConfigToml, }; + +#[cfg(feature = "acme")] +pub use parse::build_acme_manager; diff --git a/rpxy-bin/src/config/parse.rs b/rpxy-bin/src/config/parse.rs index f45ca17..4a28bed 100644 --- a/rpxy-bin/src/config/parse.rs +++ b/rpxy-bin/src/config/parse.rs @@ -6,6 +6,9 @@ use rpxy_certs::{build_cert_reloader, CryptoFileSourceBuilder, CryptoReloader, S use rpxy_lib::{AppConfig, AppConfigList, ProxyConfig}; use rustc_hash::FxHashMap as HashMap; +#[cfg(feature = "acme")] +use rpxy_acme::{AcmeTargets, ACME_CERTIFICATE_FILE_NAME, ACME_PRIVATE_KEY_FILE_NAME, ACME_REGISTRY_PATH}; + /// Parsed options pub struct Opts { pub config_file_path: String, @@ -103,11 +106,34 @@ pub async fn build_cert_manager( if config.listen_port_tls.is_none() { return Ok(None); } + + #[cfg(feature = "acme")] + let acme_option = config.experimental.as_ref().and_then(|v| v.acme.clone()); + #[cfg(feature = "acme")] + let registry_path = acme_option + .as_ref() + .and_then(|v| v.registry_path.as_deref()) + .unwrap_or(ACME_REGISTRY_PATH); + let mut crypto_source_map = HashMap::default(); for app in apps.0.values() { if let Some(tls) = app.tls.as_ref() { - ensure!(tls.tls_cert_key_path.is_some() && tls.tls_cert_path.is_some()); let server_name = app.server_name.as_ref().ok_or(anyhow!("No server name"))?; + + #[cfg(not(feature = "acme"))] + ensure!(tls.tls_cert_key_path.is_some() && tls.tls_cert_path.is_some()); + + #[cfg(feature = "acme")] + let tls = { + let mut tls = tls.clone(); + if let Some(true) = tls.acme { + ensure!(acme_option.is_some() && tls.tls_cert_key_path.is_none() && tls.tls_cert_path.is_none()); + tls.tls_cert_key_path = Some(format!("{registry_path}/{server_name}/{ACME_CERTIFICATE_FILE_NAME}")); + tls.tls_cert_path = Some(format!("{registry_path}/{server_name}/{ACME_PRIVATE_KEY_FILE_NAME}")); + } + tls + }; + let crypto_file_source = CryptoFileSourceBuilder::default() .tls_cert_path(tls.tls_cert_path.as_ref().unwrap()) .tls_cert_key_path(tls.tls_cert_key_path.as_ref().unwrap()) @@ -119,3 +145,33 @@ pub async fn build_cert_manager( let res = build_cert_reloader(&crypto_source_map, None).await?; Ok(Some(res)) } + +/* ----------------------- */ +#[cfg(feature = "acme")] +/// Build acme manager and dummy cert and key as initial states if not exists +/// TODO: CURRENTLY NOT IMPLEMENTED, UNDER DESIGNING +pub async fn build_acme_manager(config: &ConfigToml) -> Result<(), anyhow::Error> { + let acme_option = config.experimental.as_ref().and_then(|v| v.acme.clone()); + if acme_option.is_none() { + return Ok(()); + } + let acme_option = acme_option.unwrap(); + let mut acme_targets = AcmeTargets::try_new( + acme_option.email.as_ref(), + acme_option.dir_url.as_deref(), + acme_option.registry_path.as_deref(), + ) + .map_err(|e| anyhow!("Invalid acme configuration: {e}"))?; + + let apps = config.apps.as_ref().unwrap(); + for app in apps.0.values() { + if let Some(tls) = app.tls.as_ref() { + if tls.acme.unwrap_or(false) { + acme_targets.add_target(app.server_name.as_ref().unwrap())?; + } + } + } + // TODO: remove later + println!("ACME targets: {:#?}", acme_targets); + Ok(()) +} diff --git a/rpxy-bin/src/config/toml.rs b/rpxy-bin/src/config/toml.rs index 957296c..b2a70bb 100644 --- a/rpxy-bin/src/config/toml.rs +++ b/rpxy-bin/src/config/toml.rs @@ -41,12 +41,25 @@ pub struct CacheOption { pub max_cache_each_size_on_memory: Option, } +#[cfg(feature = "acme")] +#[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] +pub struct AcmeOption { + pub dir_url: Option, + pub email: String, + pub registry_path: Option, +} + #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] pub struct Experimental { #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] pub h3: Option, + #[cfg(feature = "cache")] pub cache: Option, + + #[cfg(feature = "acme")] + pub acme: Option, + pub ignore_sni_consistency: Option, pub connection_handling_timeout: Option, } @@ -67,6 +80,8 @@ pub struct TlsOption { pub tls_cert_key_path: Option, pub https_redirection: Option, pub client_ca_cert_path: Option, + #[cfg(feature = "acme")] + pub acme: Option, } #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] @@ -222,8 +237,19 @@ impl Application { // tls settings let tls_config = if self.tls.is_some() { let tls = self.tls.as_ref().unwrap(); + + #[cfg(not(feature = "acme"))] ensure!(tls.tls_cert_key_path.is_some() && tls.tls_cert_path.is_some()); + #[cfg(feature = "acme")] + { + if tls.acme.unwrap_or(false) { + ensure!(tls.tls_cert_key_path.is_none() && tls.tls_cert_path.is_none()); + } else { + ensure!(tls.tls_cert_key_path.is_some() && tls.tls_cert_path.is_some()); + } + } + let https_redirection = if tls.https_redirection.is_none() { true // Default true } else { @@ -233,6 +259,8 @@ impl Application { Some(TlsConfig { mutual_tls: tls.client_ca_cert_path.is_some(), https_redirection, + #[cfg(feature = "acme")] + acme: tls.acme.unwrap_or(false), }) } else { None diff --git a/rpxy-bin/src/main.rs b/rpxy-bin/src/main.rs index d3988e3..9847a5f 100644 --- a/rpxy-bin/src/main.rs +++ b/rpxy-bin/src/main.rs @@ -6,6 +6,8 @@ mod constants; mod error; mod log; +#[cfg(feature = "acme")] +use crate::config::build_acme_manager; use crate::{ config::{build_cert_manager, build_settings, parse_opts, ConfigToml, ConfigTomlReloader}, constants::CONFIG_WATCH_DELAY_SECS, @@ -66,6 +68,9 @@ async fn rpxy_service_without_watcher( let config_toml = ConfigToml::new(config_file_path).map_err(|e| anyhow!("Invalid toml file: {e}"))?; let (proxy_conf, app_conf) = build_settings(&config_toml).map_err(|e| anyhow!("Invalid configuration: {e}"))?; + #[cfg(feature = "acme")] // TODO: CURRENTLY NOT IMPLEMENTED, UNDER DESIGNING + let acme_manager = build_acme_manager(&config_toml).await; + let cert_service_and_rx = build_cert_manager(&config_toml) .await .map_err(|e| anyhow!("Invalid cert configuration: {e}"))?; @@ -88,6 +93,9 @@ async fn rpxy_service_with_watcher( .ok_or(anyhow!("Something wrong in config reloader receiver"))?; let (mut proxy_conf, mut app_conf) = build_settings(&config_toml).map_err(|e| anyhow!("Invalid configuration: {e}"))?; + #[cfg(feature = "acme")] // TODO: CURRENTLY NOT IMPLEMENTED, UNDER DESIGNING + let acme_manager = build_acme_manager(&config_toml).await; + let mut cert_service_and_rx = build_cert_manager(&config_toml) .await .map_err(|e| anyhow!("Invalid cert configuration: {e}"))?; diff --git a/rpxy-certs/src/reloader_service.rs b/rpxy-certs/src/reloader_service.rs index c3d1fcd..4d10fa1 100644 --- a/rpxy-certs/src/reloader_service.rs +++ b/rpxy-certs/src/reloader_service.rs @@ -46,10 +46,13 @@ impl Reload for CryptoReloader { let mut server_crypto_base = ServerCryptoBase::default(); for (server_name_bytes, crypto_source) in self.inner.iter() { - let certs_keys = crypto_source.read().await.map_err(|e| { - error!("Failed to reload cert, key or ca cert: {e}"); - ReloaderError::::Reload("Failed to reload cert, key or ca cert") - })?; + let certs_keys = match crypto_source.read().await { + Ok(certs_keys) => certs_keys, + Err(e) => { + error!("Failed to read certs and keys, skip at this time: {}", e); + continue; + } + }; server_crypto_base.inner.insert(server_name_bytes.clone(), certs_keys); } diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index e9a4666..1fbeb11 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -13,8 +13,8 @@ publish.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -# default = ["http3-s2n", "sticky-cookie", "cache", "rustls-backend"] -default = ["http3-quinn", "sticky-cookie", "cache", "rustls-backend"] +# default = ["http3-s2n", "sticky-cookie", "cache", "rustls-backend", "acme"] +# default = ["http3-quinn", "sticky-cookie", "cache", "rustls-backend", "acme"] http3-quinn = ["socket2", "quinn", "h3", "h3-quinn", "rpxy-certs/http3"] http3-s2n = [ "s2n-quic", @@ -29,6 +29,7 @@ sticky-cookie = ["base64", "sha2", "chrono"] native-tls-backend = ["hyper-tls"] rustls-backend = ["hyper-rustls"] webpki-roots = ["rustls-backend", "hyper-rustls/webpki-tokio"] +acme = [] [dependencies] rand = "0.8.5" diff --git a/rpxy-lib/src/globals.rs b/rpxy-lib/src/globals.rs index 3582cdb..fa19f78 100644 --- a/rpxy-lib/src/globals.rs +++ b/rpxy-lib/src/globals.rs @@ -159,4 +159,6 @@ pub struct UpstreamUri { pub struct TlsConfig { pub mutual_tls: bool, pub https_redirection: bool, + #[cfg(feature = "acme")] + pub acme: bool, }