diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a74b1a..4302a589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## 0.9.0 (Unreleased) +### Important Changes + +- Breaking: Experimental ACME support is added. Check the new configuration options and README.md for ACME support. Note that it is still under development and may have some issues. + +### Improvement + +- Refactor: lots of minor improvements +- Deps + ## 0.8.1 ### Improvement diff --git a/README.md b/README.md index 20d7891f..b825cd14 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# rpxy: A simple and ultrafast reverse-proxy serving multiple domain names with TLS termination, written in pure Rust +# rpxy: A simple and ultrafast reverse-proxy serving multiple domain names with TLS termination, written in Rust [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) ![Unit Test](https://github.com/junkurihara/rust-rpxy/actions/workflows/ci.yml/badge.svg) @@ -10,9 +10,11 @@ ## Introduction -`rpxy` [ahr-pik-see] is an implementation of simple and lightweight reverse-proxy with some additional features. The implementation is based on [`hyper`](https://github.com/hyperium/hyper), [`rustls`](https://github.com/rustls/rustls) and [`tokio`](https://github.com/tokio-rs/tokio), i.e., written in pure Rust. Our `rpxy` routes multiple host names to appropriate backend application servers while serving TLS connections. +`rpxy` [ahr-pik-see] is an implementation of simple and lightweight reverse-proxy with some additional features. The implementation is based on [`hyper`](https://github.com/hyperium/hyper), [`rustls`](https://github.com/rustls/rustls) and [`tokio`](https://github.com/tokio-rs/tokio), i.e., written in Rust^[^pure_rust]. Our `rpxy` routes multiple host names to appropriate backend application servers while serving TLS connections. - As default, `rpxy` provides the *TLS connection sanitization* by correctly binding a certificate used to establish a secure channel with the backend application. Specifically, it always keeps the consistency between the given SNI (server name indication) in `ClientHello` of the underlying TLS and the domain name given by the overlaid HTTP HOST header (or URL in Request line) [^1]. Additionally, as a somewhat unstable feature, our `rpxy` can handle the brand-new HTTP/3 connection thanks to [`quinn`](https://github.com/quinn-rs/quinn), [`s2n-quic`](https://github.com/aws/s2n-quic) and [`hyperium/h3`](https://github.com/hyperium/h3).[^h3lib] +[^pure_rust]: Doubtfully can be claimed to be written in pure Rust since current `rpxy` is based on `aws-lc-rs` for cryptographic operations. + + As default, `rpxy` provides the *TLS connection sanitization* by correctly binding a certificate used to establish a secure channel with the backend application. Specifically, it always keeps the consistency between the given SNI (server name indication) in `ClientHello` of the underlying TLS and the domain name given by the overlaid HTTP HOST header (or URL in Request line) [^1]. Additionally, as a somewhat unstable feature, our `rpxy` can handle the brand-new HTTP/3 connection thanks to [`quinn`](https://github.com/quinn-rs/quinn), [`s2n-quic`](https://github.com/aws/s2n-quic) and [`hyperium/h3`](https://github.com/hyperium/h3).[^h3lib] Furthermore, `rpxy` supports the automatic issuance and renewal of certificates via [TLS-ALPN-01 (RFC8737)](https://www.rfc-editor.org/rfc/rfc8737) of [ACME protocol (RFC8555)](https://www.rfc-editor.org/rfc/rfc8555) thanks to [`rustls-acme`](https://github.com/FlorianUekermann/rustls-acme). [^h3lib]: HTTP/3 libraries are mutually exclusive. You need to explicitly specify `s2n-quic` with `--no-default-features` flag. Also note that if you build `rpxy` with `s2n-quic`, then it requires `openssl` just for building the package. @@ -298,6 +300,32 @@ max_cache_each_size_on_memory = 4096 # optional. default is 4k if 0, it is alway A *storable* (in the context of an HTTP message) response is stored if its size is less than or equal to `max_cache_each_size` in bytes. If it is also less than or equal to `max_cache_each_size_on_memory`, it is stored as an on-memory object. Otherwise, it is stored as a temporary file. Note that `max_cache_each_size` must be larger or equal to `max_cache_each_size_on_memory`. Also note that once `rpxy` restarts or the config is updated, the cache is totally eliminated not only from the on-memory table but also from the file system. +### Automated Certificate Issuance and Renewal via TLS-ALPN-01 ACME protocol + +This is a brand-new feature and maybe still unstable. Thanks to the [`rustls-acme`](https://github.com/FlorianUekermann/rustls-acme), the automatic issuance and renewal of certificates are finally available in `rpxy`. To enable this feature, you need to specify the following entries in `config.toml`. + +```toml +# ACME enabled domain name. +# ACME will be used to get a certificate for the server_name with ACME tls-alpn-01 protocol. +# Note that acme option must be specified in the experimental section. +[apps.localhost_with_acme] +server_name = 'example.org' +reverse_proxy = [{ upstream = [{ location = 'example.com', tls = true }] }] +tls = { https_redirection = true, acme = true } # do not specify tls_cert_path and/or tls_cert_key_path +``` + +For the ACME enabled domain, the following settings are referred to acquire a certificate. + +```toml +# Global ACME settings. Unless specified, ACME is disabled. +[experimental.acme] +dir_url = "https://localhost:14000/dir" # optional. default is "https://acme-v02.api.letsencrypt.org/directory" +email = "test@example.com" +registry_path = "./acme_registry" # optional. default is "./acme_registry" relative to the current working directory +``` + +The above configuration is common to all ACME enabled domains. Note that the https port must be open to the public to verify the domain ownership. + ## TIPS ### Using Private Key Issued by Let's Encrypt diff --git a/config-example.toml b/config-example.toml index c3d1e476..d279e50c 100644 --- a/config-example.toml +++ b/config-example.toml @@ -89,6 +89,14 @@ server_name = 'localhost.localdomain' reverse_proxy = [{ upstream = [{ location = 'www.google.com', tls = true }] }] ###################################################################### +###################################################################### +# ACME enabled example. ACME will be used to get a certificate for the server_name with ACME tls-alpn-01 protocol. +# Note that acme option must be specified in the experimental section. +[apps.localhost_with_acme] +server_name = 'kubernetes.docker.internal' +reverse_proxy = [{ upstream = [{ location = 'example.com', tls = true }] }] +tls = { https_redirection = true, acme = true } + ################################### # Experimantal settings # ################################### @@ -119,3 +127,9 @@ cache_dir = './cache' # optional. default is "./cache" relative t max_cache_entry = 1000 # optional. default is 1k max_cache_each_size = 65535 # optional. default is 64k max_cache_each_size_on_memory = 4096 # optional. default is 4k if 0, it is always file cache. + +# ACME settings. Unless specified, ACME is disabled. +[experimental.acme] +dir_url = "https://localhost:14000/dir" # optional. default is "https://acme-v02.api.letsencrypt.org/directory" +email = "test@example.com" +registry_path = "./acme_registry" # optional. default is "./acme_registry" relative to the current working directory diff --git a/rpxy-acme/src/lib.rs b/rpxy-acme/src/lib.rs index 246b9008..6254a865 100644 --- a/rpxy-acme/src/lib.rs +++ b/rpxy-acme/src/lib.rs @@ -12,3 +12,7 @@ pub use constants::{ACME_DIR_URL, ACME_REGISTRY_PATH}; pub use dir_cache::DirCache; pub use error::RpxyAcmeError; pub use manager::AcmeManager; + +pub mod reexports { + pub use rustls_acme::is_tls_alpn_challenge; +} diff --git a/rpxy-acme/src/manager.rs b/rpxy-acme/src/manager.rs index 54b1ece9..e54731c9 100644 --- a/rpxy-acme/src/manager.rs +++ b/rpxy-acme/src/manager.rs @@ -72,9 +72,10 @@ impl AcmeManager { /// Start ACME manager to manage certificates for each domain. /// Returns a Vec> as a tasks handles and a map of domain to ServerConfig for challenge. - pub fn spawn_manager_tasks(&self) -> (Vec>, HashMap>) { - info!("rpxy ACME manager started"); - + pub fn spawn_manager_tasks( + &self, + term_notify: Option>, + ) -> (Vec>, HashMap>) { let rustls_client_config = rustls::ClientConfig::builder() .dangerous() // The `Verifier` we're using is actually safe .with_custom_certificate_verifier(Arc::new(rustls_platform_verifier::Verifier::new())) @@ -94,17 +95,30 @@ impl AcmeManager { .client_tls_config(rustls_client_config.clone()); let mut state = config.state(); server_configs_for_challenge.insert(domain.to_ascii_lowercase(), state.challenge_rustls_config()); - self.runtime_handle.spawn(async move { - info!("rpxy ACME manager task for {domain} started"); - // infinite loop unless the return value is None - loop { - let Some(res) = state.next().await else { - error!("rpxy ACME manager task for {domain} exited"); - break; + self.runtime_handle.spawn({ + let term_notify = term_notify.clone(); + async move { + info!("rpxy ACME manager task for {domain} started"); + // infinite loop unless the return value is None + let task = async { + loop { + let Some(res) = state.next().await else { + error!("rpxy ACME manager task for {domain} exited"); + break; + }; + match res { + Ok(ok) => info!("rpxy ACME event: {ok:?}"), + Err(err) => error!("rpxy ACME error: {err:?}"), + } + } }; - match res { - Ok(ok) => info!("rpxy ACME event: {ok:?}"), - Err(err) => error!("rpxy ACME error: {err:?}"), + if let Some(notify) = term_notify.as_ref() { + tokio::select! { + _ = task => {}, + _ = notify.notified() => { info!("rpxy ACME manager task for {domain} terminated") } + } + } else { + task.await; } } }) diff --git a/rpxy-bin/src/main.rs b/rpxy-bin/src/main.rs index f07212e3..eff2648b 100644 --- a/rpxy-bin/src/main.rs +++ b/rpxy-bin/src/main.rs @@ -15,7 +15,7 @@ use crate::{ log::*, }; use hot_reload::{ReloaderReceiver, ReloaderService}; -use rpxy_lib::entrypoint; +use rpxy_lib::{entrypoint, RpxyOptions, RpxyOptionsBuilder}; fn main() { init_logger(); @@ -68,30 +68,40 @@ 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")] - let acme_manager = build_acme_manager(&config_toml, runtime_handle.clone()).await?; - - let cert_service_and_rx = build_cert_manager(&config_toml) + let (cert_service, cert_rx) = build_cert_manager(&config_toml) .await - .map_err(|e| anyhow!("Invalid cert configuration: {e}"))?; + .map_err(|e| anyhow!("Invalid cert configuration: {e}"))? + .map(|(s, r)| (Some(s), Some(r))) + .unwrap_or((None, None)); #[cfg(feature = "acme")] { - rpxy_entrypoint( - &proxy_conf, - &app_conf, - cert_service_and_rx.as_ref(), - acme_manager.as_ref(), - &runtime_handle, - None, - ) - .await - .map_err(|e| anyhow!(e)) + let acme_manager = build_acme_manager(&config_toml, runtime_handle.clone()).await?; + let (acme_join_handles, server_config_acme_challenge) = acme_manager + .as_ref() + .map(|m| m.spawn_manager_tasks(None)) + .unwrap_or((vec![], Default::default())); + let rpxy_opts = RpxyOptionsBuilder::default() + .proxy_config(proxy_conf) + .app_config_list(app_conf) + .cert_rx(cert_rx) + .runtime_handle(runtime_handle.clone()) + .server_configs_acme_challenge(std::sync::Arc::new(server_config_acme_challenge)) + .build()?; + rpxy_entrypoint(&rpxy_opts, cert_service.as_ref(), acme_join_handles) //, &runtime_handle) + .await + .map_err(|e| anyhow!(e)) } #[cfg(not(feature = "acme"))] { - rpxy_entrypoint(&proxy_conf, &app_conf, cert_service_and_rx.as_ref(), &runtime_handle, None) + let rpxy_opts = RpxyOptionsBuilder::default() + .proxy_config(proxy_conf.clone()) + .app_config_list(app_conf.clone()) + .cert_rx(cert_rx.clone()) + .runtime_handle(runtime_handle.clone()) + .build()?; + rpxy_entrypoint(&rpxy_opts, cert_service.as_ref()) //, &runtime_handle) .await .map_err(|e| anyhow!(e)) } @@ -111,7 +121,7 @@ async fn rpxy_service_with_watcher( let (mut proxy_conf, mut app_conf) = build_settings(&config_toml).map_err(|e| anyhow!("Invalid configuration: {e}"))?; #[cfg(feature = "acme")] - let acme_manager = build_acme_manager(&config_toml, runtime_handle.clone()).await?; + let mut acme_manager = build_acme_manager(&config_toml, runtime_handle.clone()).await?; let mut cert_service_and_rx = build_cert_manager(&config_toml) .await @@ -122,15 +132,48 @@ async fn rpxy_service_with_watcher( // Continuous monitoring loop { + let (cert_service, cert_rx) = cert_service_and_rx + .as_ref() + .map(|(s, r)| (Some(s), Some(r))) + .unwrap_or((None, None)); + + #[cfg(feature = "acme")] + let (acme_join_handles, server_config_acme_challenge) = acme_manager + .as_ref() + .map(|m| m.spawn_manager_tasks(Some(term_notify.clone()))) + .unwrap_or((vec![], Default::default())); + + let rpxy_opts = { + #[cfg(feature = "acme")] + let res = RpxyOptionsBuilder::default() + .proxy_config(proxy_conf.clone()) + .app_config_list(app_conf.clone()) + .cert_rx(cert_rx.cloned()) + .runtime_handle(runtime_handle.clone()) + .term_notify(Some(term_notify.clone())) + .server_configs_acme_challenge(std::sync::Arc::new(server_config_acme_challenge)) + .build(); + + #[cfg(not(feature = "acme"))] + let res = RpxyOptionsBuilder::default() + .proxy_config(proxy_conf.clone()) + .app_config_list(app_conf.clone()) + .cert_rx(cert_rx.cloned()) + .runtime_handle(runtime_handle.clone()) + .term_notify(Some(term_notify.clone())) + .build(); + res + }?; + tokio::select! { rpxy_res = { #[cfg(feature = "acme")] { - rpxy_entrypoint(&proxy_conf, &app_conf, cert_service_and_rx.as_ref(), acme_manager.as_ref(), &runtime_handle, Some(term_notify.clone())) + rpxy_entrypoint(&rpxy_opts, cert_service, acme_join_handles)//, &runtime_handle) } #[cfg(not(feature = "acme"))] { - rpxy_entrypoint(&proxy_conf, &app_conf, cert_service_and_rx.as_ref(), &runtime_handle, Some(term_notify.clone())) + rpxy_entrypoint(&rpxy_opts, cert_service)//, &runtime_handle) } } => { error!("rpxy entrypoint or cert service exited"); @@ -159,8 +202,20 @@ async fn rpxy_service_with_watcher( continue; } }; + #[cfg(feature = "acme")] + { + match build_acme_manager(&config_toml, runtime_handle.clone()).await { + Ok(m) => { + acme_manager = m; + }, + Err(e) => { + error!("Invalid acme configuration. Configuration does not updated: {e}"); + continue; + } + } + } - info!("Configuration updated. Terminate all spawned proxy services and force to re-bind TCP/UDP sockets"); + info!("Configuration updated. Terminate all spawned services and force to re-bind TCP/UDP sockets"); term_notify.notify_waiters(); // tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } @@ -174,18 +229,14 @@ async fn rpxy_service_with_watcher( #[cfg(not(feature = "acme"))] /// Wrapper of entry point for rpxy service with certificate management service async fn rpxy_entrypoint( - proxy_config: &rpxy_lib::ProxyConfig, - app_config_list: &rpxy_lib::AppConfigList, - cert_service_and_rx: Option<&( - ReloaderService, - ReloaderReceiver, - )>, - runtime_handle: &tokio::runtime::Handle, - term_notify: Option>, + rpxy_opts: &RpxyOptions, + cert_service: Option<&ReloaderService>, + // runtime_handle: &tokio::runtime::Handle, ) -> Result<(), anyhow::Error> { - if let Some((cert_service, cert_rx)) = cert_service_and_rx { + // TODO: refactor: update routine + if let Some(cert_service) = cert_service { tokio::select! { - rpxy_res = entrypoint(proxy_config, app_config_list, Some(cert_rx), runtime_handle, term_notify) => { + rpxy_res = entrypoint(rpxy_opts) => { error!("rpxy entrypoint exited"); rpxy_res.map_err(|e| anyhow!(e)) } @@ -195,46 +246,49 @@ async fn rpxy_entrypoint( } } } else { - entrypoint(proxy_config, app_config_list, None, runtime_handle, term_notify) - .await - .map_err(|e| anyhow!(e)) + entrypoint(rpxy_opts).await.map_err(|e| anyhow!(e)) } } #[cfg(feature = "acme")] /// Wrapper of entry point for rpxy service with certificate management service async fn rpxy_entrypoint( - proxy_config: &rpxy_lib::ProxyConfig, - app_config_list: &rpxy_lib::AppConfigList, - cert_service_and_rx: Option<&( - ReloaderService, - ReloaderReceiver, - )>, - acme_manager: Option<&rpxy_acme::AcmeManager>, - runtime_handle: &tokio::runtime::Handle, - term_notify: Option>, + rpxy_opts: &RpxyOptions, + cert_service: Option<&ReloaderService>, + acme_task_handles: Vec>, + // runtime_handle: &tokio::runtime::Handle, ) -> Result<(), anyhow::Error> { - // TODO: remove later, reconsider routine - println!("ACME manager:\n{:#?}", acme_manager); - let x = acme_manager.unwrap().clone(); - let (handle, confs) = x.spawn_manager_tasks(); - tokio::spawn(async move { futures_util::future::select_all(handle).await }); - // TODO: - - if let Some((cert_service, cert_rx)) = cert_service_and_rx { - tokio::select! { - rpxy_res = entrypoint(proxy_config, app_config_list, Some(cert_rx), runtime_handle, term_notify) => { - error!("rpxy entrypoint exited"); - rpxy_res.map_err(|e| anyhow!(e)) + // TODO: refactor: update routine + if let Some(cert_service) = cert_service { + if acme_task_handles.is_empty() { + tokio::select! { + rpxy_res = entrypoint(rpxy_opts) => { + error!("rpxy entrypoint exited"); + rpxy_res.map_err(|e| anyhow!(e)) + } + cert_res = cert_service.start() => { + error!("cert reloader service exited"); + cert_res.map_err(|e| anyhow!(e)) + } } - cert_res = cert_service.start() => { - error!("cert reloader service exited"); - cert_res.map_err(|e| anyhow!(e)) + } else { + let select_all = futures_util::future::select_all(acme_task_handles); + tokio::select! { + rpxy_res = entrypoint(rpxy_opts) => { + error!("rpxy entrypoint exited"); + rpxy_res.map_err(|e| anyhow!(e)) + } + (acme_res, _, _) = select_all => { + error!("acme manager exited"); + acme_res.map_err(|e| anyhow!(e)) + } + cert_res = cert_service.start() => { + error!("cert reloader service exited"); + cert_res.map_err(|e| anyhow!(e)) + } } } } else { - entrypoint(proxy_config, app_config_list, None, runtime_handle, term_notify) - .await - .map_err(|e| anyhow!(e)) + entrypoint(rpxy_opts).await.map_err(|e| anyhow!(e)) } } diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index 67f5a8de..a23e192b 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -29,7 +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 = [] +acme = ["dep:rpxy-acme"] [dependencies] rand = "0.8.5" @@ -80,6 +80,9 @@ hot_reload = "0.1.6" rustls = { version = "0.23.11", default-features = false } tokio-rustls = { version = "0.26.0", features = ["early-data"] } +# acme +rpxy-acme = { path = "../rpxy-acme/", default-features = false, optional = true } + # logging tracing = { version = "0.1.40" } diff --git a/rpxy-lib/src/error.rs b/rpxy-lib/src/error.rs index 98ebf031..20470ede 100644 --- a/rpxy-lib/src/error.rs +++ b/rpxy-lib/src/error.rs @@ -105,4 +105,9 @@ pub enum RpxyError { // Others #[error("Infallible")] Infallible(#[from] std::convert::Infallible), + + /// No Acme server config for Acme challenge + #[cfg(feature = "acme")] + #[error("No Acme server config")] + NoAcmeServerConfig, } diff --git a/rpxy-lib/src/globals.rs b/rpxy-lib/src/globals.rs index fa19f787..8c5e0937 100644 --- a/rpxy-lib/src/globals.rs +++ b/rpxy-lib/src/globals.rs @@ -16,6 +16,10 @@ pub struct Globals { pub term_notify: Option>, /// Shared context - Certificate reloader service receiver // TODO: newer one pub cert_reloader_rx: Option>, + + #[cfg(feature = "acme")] + /// ServerConfig used for only ACME challenge for ACME domains + pub server_configs_acme_challenge: Arc>>, } /// Configuration parameters for proxy transport and request handlers diff --git a/rpxy-lib/src/lib.rs b/rpxy-lib/src/lib.rs index ccb647e9..9dd78da5 100644 --- a/rpxy-lib/src/lib.rs +++ b/rpxy-lib/src/lib.rs @@ -30,13 +30,36 @@ pub mod reexports { pub use hyper::Uri; } +#[derive(derive_builder::Builder)] +/// rpxy entrypoint args +pub struct RpxyOptions { + /// Configuration parameters for proxy transport and request handlers + pub proxy_config: ProxyConfig, + /// List of application configurations + pub app_config_list: AppConfigList, + /// Certificate reloader service receiver + pub cert_rx: Option>, // TODO: + /// Async task runtime handler + pub runtime_handle: tokio::runtime::Handle, + /// Notify object to stop async tasks + pub term_notify: Option>, + + #[cfg(feature = "acme")] + /// ServerConfig used for only ACME challenge for ACME domains + pub server_configs_acme_challenge: Arc>>, +} + /// Entrypoint that creates and spawns tasks of reverse proxy services pub async fn entrypoint( - proxy_config: &ProxyConfig, - app_config_list: &AppConfigList, - cert_rx: Option<&ReloaderReceiver>, // TODO: - runtime_handle: &tokio::runtime::Handle, - term_notify: Option>, + RpxyOptions { + proxy_config, + app_config_list, + cert_rx, // TODO: + runtime_handle, + term_notify, + #[cfg(feature = "acme")] + server_configs_acme_challenge, + }: &RpxyOptions, ) -> RpxyResult<()> { #[cfg(all(feature = "http3-quinn", feature = "http3-s2n"))] warn!("Both \"http3-quinn\" and \"http3-s2n\" features are enabled. \"http3-quinn\" will be used"); @@ -85,7 +108,10 @@ pub async fn entrypoint( request_count: Default::default(), runtime_handle: runtime_handle.clone(), term_notify: term_notify.clone(), - cert_reloader_rx: cert_rx.cloned(), + cert_reloader_rx: cert_rx.clone(), + + #[cfg(feature = "acme")] + server_configs_acme_challenge: server_configs_acme_challenge.clone(), }); // 3. build message handler containing Arc-ed http_client and backends, and make it contained in Arc as well diff --git a/rpxy-lib/src/proxy/proxy_main.rs b/rpxy-lib/src/proxy/proxy_main.rs index 21b2c6bd..e57a16f9 100644 --- a/rpxy-lib/src/proxy/proxy_main.rs +++ b/rpxy-lib/src/proxy/proxy_main.rs @@ -167,6 +167,9 @@ where let mut server_crypto_map: Option> = None; loop { + #[cfg(feature = "acme")] + let server_configs_acme_challenge = self.globals.server_configs_acme_challenge.clone(); + select! { tcp_cnx = tcp_listener.accept().fuse() => { if tcp_cnx.is_err() || server_crypto_map.is_none() { @@ -190,11 +193,35 @@ where if server_name.is_none(){ return Err(RpxyError::NoServerNameInClientHello); } - let server_crypto = sc_map_inner.as_ref().unwrap().get(server_name.as_ref().unwrap()); - if server_crypto.is_none() { - return Err(RpxyError::NoTlsServingApp(server_name.as_ref().unwrap().try_into().unwrap_or_default())); - } - let stream = match start.into_stream(server_crypto.unwrap().clone()).await { + /* ------------------ */ + // Check for ACME TLS ALPN challenge + #[cfg(feature = "acme")] + let server_crypto = { + if rpxy_acme::reexports::is_tls_alpn_challenge(&client_hello) { + info!("ACME TLS ALPN challenge received"); + let Some(server_crypto_acme) = server_configs_acme_challenge.get(&sni.unwrap().to_ascii_lowercase()) else { + return Err(RpxyError::NoAcmeServerConfig); + }; + server_crypto_acme + } else { + let server_crypto = sc_map_inner.as_ref().unwrap().get(server_name.as_ref().unwrap()); + let Some(server_crypto) = server_crypto else { + return Err(RpxyError::NoTlsServingApp(server_name.as_ref().unwrap().try_into().unwrap_or_default())); + }; + server_crypto + } + }; + /* ------------------ */ + #[cfg(not(feature = "acme"))] + let server_crypto = { + let server_crypto = sc_map_inner.as_ref().unwrap().get(server_name.as_ref().unwrap()); + let Some(server_crypto) = server_crypto else { + return Err(RpxyError::NoTlsServingApp(server_name.as_ref().unwrap().try_into().unwrap_or_default())); + }; + server_crypto + }; + /* ------------------ */ + let stream = match start.into_stream(server_crypto.clone()).await { Ok(s) => TokioIo::new(s), Err(e) => { return Err(RpxyError::FailedToTlsHandshake(e.to_string()));