diff --git a/.github/workflows/release_docker.yml b/.github/workflows/release_docker.yml
index 3615a392..c0182729 100644
--- a/.github/workflows/release_docker.yml
+++ b/.github/workflows/release_docker.yml
@@ -45,7 +45,7 @@ jobs:
- target: "s2n"
dockerfile: ./docker/Dockerfile
build-args: |
- "CARGO_FEATURES=--no-default-features --features=http3-s2n,cache,rustls-backend"
+ "CARGO_FEATURES=--no-default-features --features=http3-s2n,cache,rustls-backend,acme"
"ADDITIONAL_DEPS=pkg-config libssl-dev cmake libclang1 gcc g++"
platforms: linux/amd64,linux/arm64
tags-suffix: "-s2n"
@@ -58,7 +58,7 @@ jobs:
dockerfile: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64
build-args: |
- "CARGO_FEATURES=--no-default-features --features=http3-quinn,cache,webpki-roots"
+ "CARGO_FEATURES=--no-default-features --features=http3-quinn,cache,webpki-roots,acme"
tags-suffix: "-webpki-roots"
# Aliases must be used only for release builds
aliases: |
@@ -68,7 +68,7 @@ jobs:
- target: "slim-webpki-roots"
dockerfile: ./docker/Dockerfile-slim
build-args: |
- "CARGO_FEATURES=--no-default-features --features=http3-quinn,cache,webpki-roots"
+ "CARGO_FEATURES=--no-default-features --features=http3-quinn,cache,webpki-roots,acme"
build-contexts: |
messense/rust-musl-cross:amd64-musl=docker-image://messense/rust-musl-cross:x86_64-musl
messense/rust-musl-cross:arm64-musl=docker-image://messense/rust-musl-cross:aarch64-musl
@@ -82,7 +82,7 @@ jobs:
- target: "s2n-webpki-roots"
dockerfile: ./docker/Dockerfile
build-args: |
- "CARGO_FEATURES=--no-default-features --features=http3-s2n,cache,webpki-roots"
+ "CARGO_FEATURES=--no-default-features --features=http3-s2n,cache,webpki-roots,acme"
"ADDITIONAL_DEPS=pkg-config libssl-dev cmake libclang1 gcc g++"
platforms: linux/amd64,linux/arm64
tags-suffix: "-s2n-webpki-roots"
diff --git a/.gitmodules b/.gitmodules
index 0d6a4041..7ff65fe1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "submodules/rusty-http-cache-semantics"]
path = submodules/rusty-http-cache-semantics
url = git@github.com:junkurihara/rusty-http-cache-semantics.git
+[submodule "submodules/rustls-acme"]
+ path = submodules/rustls-acme
+ url = git@github.com:junkurihara/rustls-acme.git
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53a74b1a..dc1bc3aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,21 @@
# CHANGELOG
-## 0.9.0 (Unreleased)
+## 0.10.0 (Unreleased)
+
+## 0.9.0
+
+### 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
+
+### Bugfix
+
+- Fix the bug that the dynamic config reload does not work properly.
## 0.8.1
diff --git a/Cargo.toml b/Cargo.toml
index 355e5b14..01c02639 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[workspace.package]
-version = "0.8.1"
+version = "0.9.0"
authors = ["Jun Kurihara"]
homepage = "https://github.com/junkurihara/rust-rpxy"
repository = "https://github.com/junkurihara/rust-rpxy"
@@ -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/README.md b/README.md
index 20d7891f..221d6be0 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.
+
+By 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.
@@ -236,20 +238,7 @@ Since it is currently a work-in-progress project, we are frequently adding new o
## Using Docker Image
-You can also use `docker` image hosted on [Docker Hub](https://hub.docker.com/r/jqtype/rpxy) and [GitHub Container Registry](https://github.com/junkurihara/rust-rpxy/pkgs/container/rust-rpxy) instead of directly executing the binary. See [`./docker/README.md`](./docker/README.md) for the differences on image tags.
-
-There are only several docker-specific environment variables.
-
-- `HOST_USER` (default: `user`): User name executing `rpxy` inside the container.
-- `HOST_UID` (default: `900`): `UID` of `HOST_USER`.
-- `HOST_GID` (default: `900`): `GID` of `HOST_USER`
-- `LOG_LEVEL=debug|info|warn|error`: Log level
-- `LOG_TO_FILE=true|false`: Enable logging to the log file `/rpxy/log/rpxy.log` using `logrotate`. You should mount `/rpxy/log` via docker volume option if enabled. The log dir and file will be owned by the `HOST_USER` with `HOST_UID:HOST_GID` on the host machine. Hence, `HOST_USER`, `HOST_UID` and `HOST_GID` should be the same as ones of the user who executes the `rpxy` docker container on the host.
-- `WATCH=true|false` (default: `false`): Activate continuous watching of the config file if true.
-
-Then, all you need is to mount your `config.toml` as `/etc/rpxy.toml` and certificates/private keys as you like through the docker volume option. **If `WATCH=true`, You need to mount a directory, e.g., `./rpxy-config/`, including `rpxy.toml` on `/rpxy/config` instead of a file to correctly track file changes**. This is a docker limitation. Even if `WATCH=false`, you can mount the dir onto `/rpxy/config` rather than `/etc/rpxy.toml`. A file mounted on `/etc/rpxy` is prioritized over a dir mounted on `/rpxy/config`.
-
-See [`docker/docker-compose.yml`](./docker/docker-compose.yml) for the detailed configuration. Note that the file path of keys and certificates must be ones in your docker container.
+You can also use `docker` image hosted on [Docker Hub](https://hub.docker.com/r/jqtype/rpxy) and [GitHub Container Registry](https://github.com/junkurihara/rust-rpxy/pkgs/container/rust-rpxy) instead of directly executing the binary. See [`./docker`](./docker/README.md) directory for more details.
## Example
@@ -298,6 +287,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
@@ -379,6 +394,10 @@ However, we found that if you want to use the brand-new UDP-based protocol, HTTP
Your docker container can receive only TCP-based connection, i.e., HTTP/2 or before, unless you manually manage the port. We see that this is weird and expect that it is a kind of bug (of docker? ubuntu? or something else?). But at least for Ubuntu 22.04LTS, you need to handle it as above.
+### Managing `rpxy` via web interface
+
+Check a third party project [`Gamerboy59/rpxy-webui`](https://github.com/Gamerboy59/rpxy-webui) to manage `rpxy` via web interface.
+
### Other TIPS
todo!
diff --git a/TODO.md b/TODO.md
index b304abd5..16890c91 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,6 +1,5 @@
# TODO List
-- Support of `rustls-0.22`.
- We need more sophistication on `Forwarder` struct to handle `h2c`.
- Cache using `lru` crate might be inefficient in terms of the speed.
- Consider more sophisticated architecture for cache
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/docker/README.md b/docker/README.md
index 74344a76..56411d09 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -1,14 +1,33 @@
# Docker Images of `rpxy`
-The `rpxy` docker images are hosted both on [Docker Hub](https://hub.docker.com/r/jqtype/rpxy) and [GitHub Container Registry](https://github.com/junkurihara/rust-rpxy/pkgs/container/rust-rpxy). Differences among tags are summarized as follows.
+The `rpxy` docker images are hosted both on [Docker Hub](https://hub.docker.com/r/jqtype/rpxy) and [GitHub Container Registry](https://github.com/junkurihara/rust-rpxy/pkgs/container/rust-rpxy).
-## Latest Builds
+## Usage
+
+There are several docker-specific environment variables.
+
+- `HOST_USER` (default: `user`): User name executing `rpxy` inside the container.
+- `HOST_UID` (default: `900`): `UID` of `HOST_USER`.
+- `HOST_GID` (default: `900`): `GID` of `HOST_USER`
+- `LOG_LEVEL=debug|info|warn|error`: Log level
+- `LOG_TO_FILE=true|false`: Enable logging to the log file `/rpxy/log/rpxy.log` using `logrotate`. You should mount `/rpxy/log` via docker volume option if enabled. The log dir and file will be owned by the `HOST_USER` with `HOST_UID:HOST_GID` on the host machine. Hence, `HOST_USER`, `HOST_UID` and `HOST_GID` should be the same as ones of the user who executes the `rpxy` docker container on the host.
+- `WATCH=true|false` (default: `false`): Activate continuous watching of the config file if true.
+
+Then, all you need is to mount your `config.toml` as `/etc/rpxy.toml` and certificates/private keys as you like through the docker volume option. **If `WATCH=true`, You need to mount a directory, e.g., `./rpxy-config/`, including `rpxy.toml` on `/rpxy/config` instead of a file to correctly track file changes**. This is a docker limitation. Even if `WATCH=false`, you can mount the dir onto `/rpxy/config` rather than `/etc/rpxy.toml`. A file mounted on `/etc/rpxy` is prioritized over a dir mounted on `/rpxy/config`.
+
+See [`docker-compose.yml`](./docker-compose.yml) for the detailed configuration. Note that the file path of keys and certificates must be ones in your docker container.
+
+## Differences among image tags of Docker Hub and GitHub Container Registry
+
+Differences among tags are summarized as follows.
+
+### Latest Builds
- `latest`: Built from the `main` branch with default features, running on Ubuntu.
- `latest-slim`, `slim`: Built by `musl` from the `main` branch with default features, running on Alpine.
- `latest-s2n`, `s2n`: Built from the `main` branch with the `http3-s2n` feature, running on Ubuntu.
-## Nightly Builds
+### Nightly Builds
- `nightly`: Built from the `develop` branch with default features, running on Ubuntu.
- `nightly-slim`: Built by `musl` from the `develop` branch with default features, running on Alpine.
@@ -17,3 +36,5 @@ The `rpxy` docker images are hosted both on [Docker Hub](https://hub.docker.com/
## Caveats
Due to some compile errors of `s2n-quic` subpackages with `musl`, `nightly-s2n-slim` or `latest-s2n-slim` are not yet provided.
+
+See [`./docker/README.md`](./docker/README.md) for the differences on image tags.
diff --git a/docker/docker-compose-slim.yml b/docker/docker-compose-slim.yml
index e02c20b4..0337d200 100644
--- a/docker/docker-compose-slim.yml
+++ b/docker/docker-compose-slim.yml
@@ -31,6 +31,7 @@ services:
volumes:
- ./log:/rpxy/log:rw
- ./cache:/rpxy/cache:rw
+ - ./acme_registry:/rpxy/acme_registry:rw
- ../example-certs/server.crt:/certs/server.crt:ro
- ../example-certs/server.key:/certs/server.key:ro
- ../config-example.toml:/etc/rpxy.toml:ro
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 435dcb35..a8ad4af3 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -31,6 +31,7 @@ services:
volumes:
- ./log:/rpxy/log:rw
- ./cache:/rpxy/cache:rw
+ - ./acme_registry:/rpxy/acme_registry:rw
- ../example-certs/server.crt:/certs/server.crt:ro
- ../example-certs/server.key:/certs/server.key:ro
- ../config-example.toml:/etc/rpxy.toml:ro
diff --git a/rpxy-acme/Cargo.toml b/rpxy-acme/Cargo.toml
new file mode 100644
index 00000000..679c94d8
--- /dev/null
+++ b/rpxy-acme/Cargo.toml
@@ -0,0 +1,34 @@
+[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.63"
+tracing = "0.1.40"
+async-trait = "0.1.81"
+base64 = "0.22.1"
+aws-lc-rs = { version = "1.8.1", default-features = false, features = [
+ "aws-lc-sys",
+] }
+blocking = "1.6.1"
+rustls = { version = "0.23.12", default-features = false, features = [
+ "std",
+ "aws_lc_rs",
+] }
+rustls-platform-verifier = { version = "0.3.3" }
+rustls-acme = { path = "../submodules/rustls-acme/", default-features = false, features = [
+ "aws-lc-rs",
+] }
+tokio = { version = "1.39.2", default-features = false }
+tokio-util = { version = "0.7.11", default-features = false }
+tokio-stream = { version = "0.1.15", default-features = false }
diff --git a/rpxy-acme/src/constants.rs b/rpxy-acme/src/constants.rs
new file mode 100644
index 00000000..7b544b0b
--- /dev/null
+++ b/rpxy-acme/src/constants.rs
@@ -0,0 +1,8 @@
+/// 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 = "accounts";
diff --git a/rpxy-acme/src/dir_cache.rs b/rpxy-acme/src/dir_cache.rs
new file mode 100644
index 00000000..5170ad61
--- /dev/null
+++ b/rpxy-acme/src/dir_cache.rs
@@ -0,0 +1,107 @@
+use crate::constants::ACME_ACCOUNT_SUBDIR;
+use async_trait::async_trait;
+use aws_lc_rs as crypto;
+use base64::prelude::*;
+use blocking::unblock;
+use crypto::digest::{Context, SHA256};
+use rustls_acme::{AccountCache, CertCache};
+use std::{
+ io::ErrorKind,
+ path::{Path, PathBuf},
+};
+
+enum FileType {
+ Account,
+ Cert,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct DirCache {
+ pub(super) account_dir: PathBuf,
+ pub(super) cert_dir: PathBuf,
+}
+
+impl DirCache {
+ pub fn new