From 537a73f900ff65a4995f2615fc634c858e64d6cd Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Sun, 7 Jun 2026 22:49:12 +0200 Subject: [PATCH 01/15] update dependencies --- Cargo.lock | 331 ++++++++++------------------------------------------- Cargo.toml | 7 +- 2 files changed, 66 insertions(+), 272 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 477111f..c080ed6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,12 +119,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - [[package]] name = "arc-swap" version = "1.9.1" @@ -401,27 +395,27 @@ dependencies = [ [[package]] name = "cf-rustracing" -version = "1.3.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6565523d8145e63e0cf1b397a5f1bd4e90d5652a7dffb2de8cec460ff23ef6b1" +checksum = "93f85c3824e4191621dec0551e3cef3d511f329da9a8990bf3e450a85651d97e" dependencies = [ "backtrace", - "rand 0.10.1", + "rand 0.8.6", "tokio", "trackable", ] [[package]] name = "cf-rustracing-jaeger" -version = "1.3.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c0e4d8cce27f6a6eaff58d2b66f063a18b8ed0d6ef0947ae7a263afa3b7c08" +checksum = "a6a5f80d44c257c3300a7f45ada676c211e64bbbac591bbec19344a8f61fbcab" dependencies = [ "cf-rustracing", "hostname", "local-ip-address", "percent-encoding", - "rand 0.10.1", + "rand 0.9.4", "thrift_codec", "tokio", "trackable", @@ -433,22 +427,11 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core 0.10.1", -] - [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -550,15 +533,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -796,12 +770,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -968,24 +936,10 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", -] - [[package]] name = "getset" version = "0.1.6" @@ -1035,15 +989,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1052,7 +997,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1063,7 +1008,7 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1216,12 +1161,6 @@ dependencies = [ "cc", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1246,8 +1185,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.0", - "serde", - "serde_core", ] [[package]] @@ -1374,12 +1311,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -1678,9 +1609,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pingora" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "844a13b16e556293f4ea96dc5ac0923ac6f36855a9dfc13b640d0da183f6b5b7" +checksum = "54a75f2ff8e122aa80ab202dc865294fe59cd856c2a5dab2d3df6e122c93b941" dependencies = [ "pingora-cache", "pingora-core", @@ -1692,9 +1623,9 @@ dependencies = [ [[package]] name = "pingora-boringssl" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42c14f7d2002b24c809a0540e13c6bce1406549d527e99c2d6ced1f32a1d2b7b" +checksum = "d63a2d970f4c6ed9bd91025d89d8756a3ce1bb17969f4c83928445210cbf4940" dependencies = [ "boring", "boring-sys", @@ -1706,9 +1637,9 @@ dependencies = [ [[package]] name = "pingora-cache" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59d8c4c939a3a193a3da0e061aa7acf7432431f92ee62a26f5a9e5167a0ade2" +checksum = "527735ac204efb9fa3884bfd9224d016c5735fabe1d394ebed145b40e7545b99" dependencies = [ "ahash", "async-trait", @@ -1743,9 +1674,9 @@ dependencies = [ [[package]] name = "pingora-core" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08973c4853cef4c682f7a592907e81a32dcad69476c4846e5de079f16448b177" +checksum = "6a7ffe2f5acf9f94fd255cfd1438866bc9124f8f0c7d42562bd3f853df2094b7" dependencies = [ "ahash", "async-trait", @@ -1795,15 +1726,15 @@ dependencies = [ [[package]] name = "pingora-error" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9fa97a500e7e5c27a7b8609b9294c8922c9656322285268bfad9520f12feb38" +checksum = "b23f7bc013de67e44ed902a82843f6157460b89d11da882bcc6f09f8ae380af1" [[package]] name = "pingora-header-serde" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2705feb8b50d4e734e0c7d3879aa040e655a45656276323ff530e254585dd816" +checksum = "828c0e53e74160cbfe8e67dd3a811eb6a253c36acbaf7a39a01d9aacfb9ac139" dependencies = [ "bytes", "http", @@ -1817,9 +1748,9 @@ dependencies = [ [[package]] name = "pingora-http" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb52d4651b687fab6abf669539cfd97b7cd94b301fde8f57c63354f9c9cc5e2" +checksum = "d553d310a15ec88107b9388a02885f798efc57764d8e9bdaaf32a76722927a10" dependencies = [ "bytes", "http", @@ -1828,18 +1759,18 @@ dependencies = [ [[package]] name = "pingora-ketama" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0286fb5a0359dca1e2e137dfe14ca4d94f676635a5eae4616bb3d8d4ce06d120" +checksum = "3e2a2e43a14f1d291fba7905542c7c1d1f89528f470b3cd48b6806e702ea772f" dependencies = [ "crc32fast", ] [[package]] name = "pingora-load-balancing" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2606e9e22e72927a69772cefe56b0d41d251c3ffdfcd548a6020fe157fb79ad" +checksum = "361b69af0234d2e4d10234e2efd106bb3b8147c575d52f45604a46aaf26def7a" dependencies = [ "arc-swap", "async-trait", @@ -1859,9 +1790,9 @@ dependencies = [ [[package]] name = "pingora-lru" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91bb5030596a3d442c0866ac68afe29c14ba558e77c726dcdf7016b0dbb359d9" +checksum = "6705a26ad89d241a989a5395641931ba37076f5ab5fbd19ee92402414a43af32" dependencies = [ "arrayvec", "hashbrown 0.17.0", @@ -1871,9 +1802,9 @@ dependencies = [ [[package]] name = "pingora-pool" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67f034be36772f318370d058913db43dbd22c3763ad974c995ba2e4afb2bb52a" +checksum = "feb1237893b15a9cf6b371bee8d7e2e1c10742e4be6eb00ed38cfe87fd1363f8" dependencies = [ "crossbeam-queue", "log", @@ -1886,9 +1817,9 @@ dependencies = [ [[package]] name = "pingora-proxy" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1e070a98a70d0d05f2fdcfb706237e06a043b2fbc9261e8772a3459cc2175e" +checksum = "8a92ee756ecf6ecb6419864da651cad6cecd933b6d420a26877031efa16bef57" dependencies = [ "async-trait", "bytes", @@ -1909,9 +1840,9 @@ dependencies = [ [[package]] name = "pingora-runtime" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e371315b1c44c2e5a8788fdc61577527b785e121e6ff49144755f40d86511430" +checksum = "41815a13691a3e7d9ad0e34767d4140284132e31b95a4481f5e73ab6f407f834" dependencies = [ "once_cell", "rand 0.8.6", @@ -1921,9 +1852,9 @@ dependencies = [ [[package]] name = "pingora-timeout" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a853fee5ce510a7f5db2561f99c752724112ed13fc3820e70d462d278d704ea" +checksum = "8e3e321452eaa461e0b6c5aaa35b7e42527ee89df33710279f37fae7f066b68e" dependencies = [ "once_cell", "parking_lot", @@ -1953,16 +1884,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2019,6 +1940,7 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" name = "proxy" version = "0.1.0" dependencies = [ + "arc-swap", "async-trait", "chrono", "http", @@ -2046,12 +1968,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" version = "0.8.6" @@ -2059,19 +1975,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.10.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -2084,6 +1999,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -2095,9 +2020,12 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.1" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] [[package]] name = "rcgen" @@ -2837,12 +2765,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2904,16 +2826,7 @@ version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -2961,40 +2874,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "webpki-root-certs" version = "1.0.7" @@ -3191,100 +3070,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "x509-parser" version = "0.18.1" diff --git a/Cargo.toml b/Cargo.toml index 4da94a9..6953af3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ unused = { level = "allow", priority = -1 } async-trait = "0.1.89" tokio = { version = "1.52.3", features = ["full"] } +# Arc +arc-swap = "1.9.1" + # Certs # used Ring as backend for default rcgen = { version = "0.14.8", features = ["default"] } @@ -36,8 +39,8 @@ serde = { version = "1.0.228", features = ["derive"] } # Others thiserror = "2.0.18" -chrono = { version = "0.4.44", features = ["serde"] } +chrono = { version = "0.4.45", features = ["serde"] } # HTTP http = "1.4.1" -pingora = { version = "0.8.0", features = ["proxy", "boringssl"] } +pingora = { version = "0.8.1", features = ["proxy", "boringssl"] } From e17ef2d1e7a713f27bd2d8718297ce4e93e70f59 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Sun, 7 Jun 2026 22:50:16 +0200 Subject: [PATCH 02/15] wire up services for certs and scaffold background renewal --- README.md | 7 ++ src/core/handlers/certs.rs | 144 +++++++++++++++++++++--------- src/core/handlers/filesystem.rs | 12 ++- src/core/handlers/proxy.rs | 63 ++++++------- src/core/models/certs.rs | 8 +- src/core/models/proxy.rs | 3 - src/core/models/tasks.rs | 1 + src/core/setup.rs | 51 +++++------ src/main.rs | 8 +- src/services/certs/acme.rs | 7 ++ src/services/certs/mod.rs | 2 + src/services/certs/self_signed.rs | 34 +++++++ src/services/mod.rs | 1 + 13 files changed, 217 insertions(+), 124 deletions(-) create mode 100644 src/services/certs/acme.rs create mode 100644 src/services/certs/mod.rs create mode 100644 src/services/certs/self_signed.rs create mode 100644 src/services/mod.rs diff --git a/README.md b/README.md index c2c738a..140042e 100644 --- a/README.md +++ b/README.md @@ -166,3 +166,10 @@ I'll be upfront on every public project about what was done with AI. For this on - Cleanup of the README and other prose - Talking through code-level solutions - Generating the Docker image scaffolding from a spec + +# TODO: + +- finish create_acme_dns_challenge +- create a dns entry checker for the background task +- generate a staging certificate with instance_acme +- wire up the full flow in CertBackgroundRenewal diff --git a/src/core/handlers/certs.rs b/src/core/handlers/certs.rs index f9d088d..6139485 100644 --- a/src/core/handlers/certs.rs +++ b/src/core/handlers/certs.rs @@ -1,47 +1,42 @@ use crate::{ Error, Result, core::{ - handlers::filesystem::{safe_path, write_file}, + handlers::filesystem::{check_file_exists, read_file, safe_path, write_file}, models::{ - certs::{CertificateConfig, CertificateType}, + certs::{ + CertDir, CertPath, CertificateConfig, CertificateType, KeyPath, TlsMaterial, + TlsStore, + }, + routes::Host, tasks::TaskInterval, }, }, info, + services::certs::{ + acme::create_acme_dns_challenge, self_signed::create_self_signed_certificate_files, + }, + warn, }; +use arc_swap::ArcSwap; +use async_trait::async_trait; +use pingora::{server::ShutdownWatch, services::background::BackgroundService, tls}; use rcgen::{CertifiedKey, generate_simple_self_signed}; -use std::time::Duration; +use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::time; -pub fn generate_certs(certificate_configs: Vec) -> Result<()> { - for config in &certificate_configs { - // self signed certificates are good until the year 4096 - // this will be replace every restart so it's safe to keep using the default setting +pub fn create_initial_certs(certificate_configs: &Vec) -> Result<()> { + for config in certificate_configs { match config.cert_type { CertificateType::SelfSigned => { - info!("generating self-signed certificate for {}", config.host); - - let subject_alt_names = vec![config.host.to_string()]; - let pem_filename = format!("{}.pem", config.host); - let key_filename = format!("{}.key", config.host); - - let key_path = safe_path(&config.cert_dir, &key_filename)?; - let pem_path = safe_path(&config.cert_dir, &pem_filename)?; - - let CertifiedKey { - cert, - signing_key, - } = generate_simple_self_signed(subject_alt_names) - .map_err(|e| Error::Certificate(e.to_string()))?; - - let pem_serialized = cert.pem(); - let key_serialized = signing_key.serialize_pem(); - - write_file(pem_path, pem_serialized.as_bytes())?; - write_file(key_path, key_serialized.as_bytes())?; + // self signed certificates are good until the year 4096 + // this will be replace every restart so it's safe to keep using the default setting + // for selfsigned we will create the certs here right away + create_self_signed_certificate_files(config); }, CertificateType::Acme => { - info!("generating acme certificate for {}", config.host); + // we do not generate any certs here that is for the backend process, + // we are only displaying the DNS entry that needs to be included to make sure you are able to get a cert + create_acme_dns_challenge(config); }, CertificateType::None => {}, } @@ -50,24 +45,87 @@ pub fn generate_certs(certificate_configs: Vec) -> Result<()> Ok(()) } -pub async fn background_certs_task( - certificates: Vec, - task_interval: TaskInterval, -) -> Result<()> { - // only acme certificates need to be renewed - let certificates = certificates - .into_iter() - .filter(|cert| cert.cert_type == CertificateType::Acme) - .collect::>(); +pub fn load_tls_store(certificate_configs: &Vec) -> Result { + let mut tls_certs = HashMap::new(); - loop { - info!("certificates: {}", certificates.len()); + for config in certificate_configs { + if config.cert_type != CertificateType::None { + let (key_path, cert_path) = certificate_paths(&config.host, &config.cert_dir)?; - // for certificate in &certificates { - // } + let has_tls_files = check_file_exists(&key_path) && check_file_exists(&cert_path); - time::sleep(Duration::from_secs(*task_interval)).await; + // We only show a warning so its easier to debug once its running, + // but we are not stopping any traffic. + if !has_tls_files { + warn!("Certificate files not found for host `{}` but is expected", &config.host); + } + + let cert_bytes = read_file(&cert_path)?; + let cert = tls::x509::X509::from_pem(&cert_bytes) + .map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?; + + let key_bytes = read_file(&key_path)?; + let key = tls::pkey::PKey::private_key_from_pem(&key_bytes) + .map_err(|e| Error::Certificate(format!("Failed to parse private key: {}", e)))?; + + tls_certs.insert( + config.host.clone(), + TlsMaterial { + cert, + key, + }, + ); + } } - Ok(()) + let tls_store: TlsStore = Arc::new(ArcSwap::from_pointee(tls_certs)); + + Ok(tls_store) +} + +pub fn certificate_paths(host: &Host, cert_dir: &CertDir) -> Result<(KeyPath, CertPath)> { + let cert_filename = format!("{}.pem", host); + let key_filename = format!("{}.key", host); + + let key_path = safe_path(cert_dir, &key_filename)?; + let cert_path = safe_path(cert_dir, &cert_filename)?; + + Ok((key_path, cert_path)) +} + +pub struct CertBackgroundRenewal { + pub certificate_configs: Vec, + pub task_interval: TaskInterval, + pub tls_store: TlsStore, +} + +impl CertBackgroundRenewal { + pub fn new( + certificate_configs: Vec, + task_interval: TaskInterval, + tls_store: TlsStore, + ) -> Self { + Self { + certificate_configs, + task_interval, + tls_store, + } + } +} + +#[async_trait] +impl BackgroundService for CertBackgroundRenewal { + async fn start(&self, mut shutdown: ShutdownWatch) { + loop { + // TODO: couple things we will do in this background task + // - renew a certificate if its needed, and update the tls_store with the new certificate data + // - check if the dns has its text entry so we can start the process of generating certificates for that domein + // + + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(*self.task_interval)) => {} + _ = shutdown.changed() => break, + } + } + } } diff --git a/src/core/handlers/filesystem.rs b/src/core/handlers/filesystem.rs index bc77a13..f4648d8 100644 --- a/src/core/handlers/filesystem.rs +++ b/src/core/handlers/filesystem.rs @@ -8,10 +8,10 @@ pub fn write_file(file_path: SafePath, content: &[u8]) -> Result<()> { // foxguard: ignore[rs/no-path-traversal] // validated with SafePath let path = Path::new(file_path.as_str()); - if let Some(parent) = path.parent() { - if !parent.exists() { - return Err(Error::FileSystem(format!("Parent directory {:?} does not exist", parent))); - } + if let Some(parent) = path.parent() + && !parent.exists() + { + return Err(Error::FileSystem(format!("Parent directory {:?} does not exist", parent))); } fs::write(file_path.as_str(), content) @@ -30,9 +30,7 @@ pub fn read_file(file_path: &SafePath) -> Result> { pub fn check_file_exists(file_path: &SafePath) -> bool { // foxguard: ignore[rs/no-path-traversal] // validated with SafePath - let exists = Path::new(file_path.as_str()).exists(); - - exists + Path::new(file_path.as_str()).exists() } /// Constructs a safe path by joining `base` and `file_path` and normalizing it. diff --git a/src/core/handlers/proxy.rs b/src/core/handlers/proxy.rs index 62693f4..a257d6f 100644 --- a/src/core/handlers/proxy.rs +++ b/src/core/handlers/proxy.rs @@ -1,21 +1,22 @@ use crate::{ Error, Result, core::{ - handlers::filesystem::read_file, + handlers::{certs::CertBackgroundRenewal, filesystem::read_file}, models::{ - certs::{TlsCerts, TlsMaterial}, + certs::{TlsMaterial, TlsStore}, proxy::{ProxyConfig, ProxyRoute, ProxyRouteMap}, }, }, error, }; +use arc_swap::ArcSwap; use async_trait::async_trait; use http::header; use pingora::{ ErrorType, http::ResponseHeader, listeners::{TlsAccept, tls::TlsSettings}, - prelude::{Error as PingoraError, HttpPeer, Result as PingoraResult}, + prelude::{Error as PingoraError, HttpPeer, Result as PingoraResult, background_service}, proxy::{ProxyHttp, Session, http_proxy_service}, server::{Server, configuration::ServerConf}, services::Service, @@ -23,7 +24,12 @@ use pingora::{ }; use std::{collections::HashMap, sync::Arc}; -pub fn run_proxy(proxy_config: ProxyConfig, routes: Vec) -> Result<()> { +pub fn run_proxy( + proxy_config: ProxyConfig, + routes: Vec, + tls_store: TlsStore, + renewal: CertBackgroundRenewal, +) -> Result<()> { let mut server = Server::new(None).map_err(|e| Error::Proxy(e.to_string()))?; server.bootstrap(); @@ -31,34 +37,13 @@ pub fn run_proxy(proxy_config: ProxyConfig, routes: Vec) -> Result<( let server_conf = server.configuration.clone(); let http_addr = format!("{}:{}", proxy_config.input_address, *proxy_config.http_port); let https_addr = format!("{}:{}", proxy_config.input_address, *proxy_config.https_port); - let mut routes_map = HashMap::new(); - let mut tls_certs = HashMap::new(); for route in routes { - if *route.tls { - let cert_bytes = read_file(&route.cert_path)?; - let cert = tls::x509::X509::from_pem(&cert_bytes) - .map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?; - - let key_bytes = read_file(&route.key_path)?; - let key = tls::pkey::PKey::private_key_from_pem(&key_bytes) - .map_err(|e| Error::Certificate(format!("Failed to parse private key: {}", e)))?; - - tls_certs.insert( - route.host.clone(), - TlsMaterial { - cert, - key, - }, - ); - } - routes_map.insert(route.host.clone(), route); } let routes_map = Arc::new(routes_map); - let tls_certs = Arc::new(tls_certs); // plain proxies with redirect let plain_service = @@ -66,10 +51,12 @@ pub fn run_proxy(proxy_config: ProxyConfig, routes: Vec) -> Result<( server.add_service(plain_service); // tls proxies - let tls_service = - tls_routes_service(server_conf, https_addr.clone(), routes_map.clone(), tls_certs)?; + let tls_service = tls_routes_service(server_conf, https_addr, routes_map, tls_store)?; server.add_service(tls_service); + // background cert services + server.add_service(background_service("cert-renewal", renewal)); + server.run_forever(); } @@ -77,14 +64,14 @@ pub fn tls_routes_service( server_conf: Arc, listen_addr: String, routes_map: ProxyRouteMap, - tls_certs: TlsCerts, + tls_store: TlsStore, ) -> Result { let proxy_app = ProxyToUpstream::new(routes_map.clone(), false); let mut service = http_proxy_service(&server_conf, proxy_app); - let sni_resolver = SniResolver::new(tls_certs); - let callback = Box::new(sni_resolver); + let cert_resolver = CertResolver::new(tls_store); + let callback = Box::new(cert_resolver); let tls_settings = TlsSettings::with_callbacks(callback).map_err(|e| Error::Proxy(e.to_string()))?; service.add_tls_with_settings(&listen_addr, None, tls_settings); @@ -186,20 +173,20 @@ fn host_from_session(session: &Session) -> Option<&str> { session.get_header(header::HOST).and_then(|h| h.to_str().ok()) } -struct SniResolver { - certs: TlsCerts, +struct CertResolver { + tls_store: TlsStore, } -impl SniResolver { - fn new(tls_certs: TlsCerts) -> Self { +impl CertResolver { + fn new(tls_store: TlsStore) -> Self { Self { - certs: tls_certs, + tls_store, } } } #[async_trait] -impl TlsAccept for SniResolver { +impl TlsAccept for CertResolver { async fn certificate_callback(&self, ssl: &mut ssl::SslRef) -> () { let sni_provided = ssl.servername(ssl::NameType::HOST_NAME).map(str::to_owned); @@ -208,10 +195,12 @@ impl TlsAccept for SniResolver { return; }; + let certs = self.tls_store.load(); + let Some(TlsMaterial { cert, key, - }) = self.certs.get(sni_provided.as_str()) + }) = certs.get(sni_provided.as_str()) else { error!("No certificate found for SNI: {}", sni_provided); return; diff --git a/src/core/models/certs.rs b/src/core/models/certs.rs index 818149a..a0546b6 100644 --- a/src/core/models/certs.rs +++ b/src/core/models/certs.rs @@ -1,4 +1,5 @@ -use crate::core::models::routes::Host; +use crate::core::models::{filesystem::SafePath, routes::Host}; +use arc_swap::ArcSwap; use pingora::tls::{ pkey::{PKey, Private}, x509::X509, @@ -31,7 +32,10 @@ pub struct TlsMaterial { pub key: PKey, } -pub type TlsCerts = Arc>; +pub type KeyPath = SafePath; +pub type CertPath = SafePath; + +pub type TlsStore = Arc>>; // --- EMAIL --- #[derive(Debug, Clone, Deserialize)] diff --git a/src/core/models/proxy.rs b/src/core/models/proxy.rs index dab8d40..ca5438f 100644 --- a/src/core/models/proxy.rs +++ b/src/core/models/proxy.rs @@ -13,13 +13,10 @@ pub struct ProxyRoute { pub host: Host, pub upstream: Upstream, pub tls: ProxyTls, - pub cert_path: SafePath, - pub key_path: SafePath, } #[derive(Debug, Clone)] pub struct ProxyConfig { - pub cert_dir: CertDir, pub http_port: ProxyPort, pub https_port: ProxyPort, pub input_address: ProxyInputAddress, diff --git a/src/core/models/tasks.rs b/src/core/models/tasks.rs index 1cc2b46..8722ed1 100644 --- a/src/core/models/tasks.rs +++ b/src/core/models/tasks.rs @@ -2,6 +2,7 @@ use serde::Deserialize; use std::ops::Deref; // --- TASK INTERVAL --- +/// Interval for a task in seconds. #[derive(Debug, Clone, Deserialize)] pub struct TaskInterval(u64); diff --git a/src/core/setup.rs b/src/core/setup.rs index b133345..1ed4ab8 100644 --- a/src/core/setup.rs +++ b/src/core/setup.rs @@ -2,12 +2,14 @@ use crate::{ Error, Result, core::{ handlers::{ - certs::generate_certs, + certs::{ + CertBackgroundRenewal, certificate_paths, create_initial_certs, load_tls_store, + }, filesystem::{check_file_exists, safe_path}, proxy::run_proxy, }, models::{ - certs::{CertDir, CertificateConfig, CertificateType, Email}, + certs::{CertDir, CertificateConfig, CertificateType, Email, TlsStore}, proxy::{ProxyConfig, ProxyRoute, ProxyTls}, routes::Route, tasks::TaskInterval, @@ -72,61 +74,54 @@ impl HandleCertificates { } } - pub fn run(self) -> Result<()> { - generate_certs(self.certificate_configs.clone())?; + pub fn run(self) -> Result<(TlsStore, CertBackgroundRenewal)> { + create_initial_certs(&self.certificate_configs)?; - // spawn(async move { - // info!("starting certificates tasks..."); - // background_certs_task(self.certificates, self.task_interval).await; - // }); + let store = load_tls_store(&self.certificate_configs)?; + let renewal = CertBackgroundRenewal::new( + self.certificate_configs.clone(), + self.task_interval.clone(), + store.clone(), + ); - Ok(()) + Ok((store, renewal)) } } pub struct HandleProxy { proxy_config: ProxyConfig, proxy_routes: Vec, + tls_store: TlsStore, } impl HandleProxy { - pub fn new(proxy_config: ProxyConfig, routes: Vec) -> Result { + pub fn new(proxy_config: ProxyConfig, routes: Vec, tls_store: TlsStore) -> Result { let mut proxy_routes = Vec::new(); for route in routes { - let cert_filename = format!("{}.pem", route.host); - let key_filename = format!("{}.key", route.host); - - let key_path = safe_path(&proxy_config.cert_dir, &key_filename)?; - let cert_path = safe_path(&proxy_config.cert_dir, &cert_filename)?; - - let has_tls_files = check_file_exists(&key_path) && check_file_exists(&cert_path); - - // We only show a warning so its easier to debug once its running, - // but we are not stopping any traffic. - if !has_tls_files && route.cert_type != CertificateType::None { - warn!("Certificate files not found for host `{}` but is expected", route.host); - } - proxy_routes.push(ProxyRoute { host: route.host.clone(), upstream: route.upstream.clone(), tls: ProxyTls::from(route.cert_type != CertificateType::None), - cert_path, - key_path, }); } Ok(Self { proxy_config, proxy_routes, + tls_store, }) } - pub fn run(&self) -> Result<()> { + pub fn run(&self, renewal: CertBackgroundRenewal) -> Result<()> { info!("proxy running..."); - run_proxy(self.proxy_config.clone(), self.proxy_routes.clone())?; + run_proxy( + self.proxy_config.clone(), + self.proxy_routes.clone(), + self.tls_store.clone(), + renewal, + )?; Err(Error::Proxy("proxy exited".to_string())) } diff --git a/src/main.rs b/src/main.rs index ad61be0..bf0e8fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ pub use error::{Error, Result}; mod config; mod core; pub mod error; +mod services; mod utils; // entry needs to be synchronous as pingora has there own @@ -29,17 +30,16 @@ fn main() -> Result<()> { config.routes.clone(), config.task_interval, ); - cert_handler.run()?; + let (store, renewal) = cert_handler.run()?; // start the proxy let proxy_config = ProxyConfig { - cert_dir: config.cert_dir.clone(), http_port: config.http_port, https_port: config.https_port, input_address: config.input_address, }; - let proxy_handler = HandleProxy::new(proxy_config, config.routes)?; - proxy_handler.run()?; + let proxy_handler = HandleProxy::new(proxy_config, config.routes, store)?; + proxy_handler.run(renewal)?; Err(Error::MainLoopClosed) } diff --git a/src/services/certs/acme.rs b/src/services/certs/acme.rs new file mode 100644 index 0000000..87a9742 --- /dev/null +++ b/src/services/certs/acme.rs @@ -0,0 +1,7 @@ +use crate::{Error, Result, core::models::certs::CertificateConfig, info}; + +pub fn create_acme_dns_challenge(config: &CertificateConfig) -> Result<()> { + info!("creating acme dns challenge for {}", config.host); + + Ok(()) +} diff --git a/src/services/certs/mod.rs b/src/services/certs/mod.rs new file mode 100644 index 0000000..7c4495a --- /dev/null +++ b/src/services/certs/mod.rs @@ -0,0 +1,2 @@ +pub mod acme; +pub mod self_signed; diff --git a/src/services/certs/self_signed.rs b/src/services/certs/self_signed.rs new file mode 100644 index 0000000..21099bf --- /dev/null +++ b/src/services/certs/self_signed.rs @@ -0,0 +1,34 @@ +use crate::{ + Error, Result, + core::{ + handlers::{ + certs::certificate_paths, + filesystem::{safe_path, write_file}, + }, + models::certs::CertificateConfig, + }, + info, +}; +use rcgen::{CertifiedKey, generate_simple_self_signed}; + +pub fn create_self_signed_certificate_files(config: &CertificateConfig) -> Result<()> { + info!("generating self-signed certificate for {}", config.host); + + let subject_alt_names = vec![config.host.to_string()]; + + let (key_path, cert_path) = certificate_paths(&config.host, &config.cert_dir)?; + + let CertifiedKey { + cert, + signing_key, + } = generate_simple_self_signed(subject_alt_names) + .map_err(|e| Error::Certificate(e.to_string()))?; + + let cert_serialized = cert.pem(); + let key_serialized = signing_key.serialize_pem(); + + write_file(cert_path, cert_serialized.as_bytes())?; + write_file(key_path, key_serialized.as_bytes())?; + + Ok(()) +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..e410b93 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1 @@ +pub mod certs; From aa27218e3eddd0873f4c407dd35ea03a62a67464 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Sun, 7 Jun 2026 22:56:20 +0200 Subject: [PATCH 03/15] clearify the port mapping in docker and how AI was used --- README.md | 4 +++- docker-compose.yml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 140042e..42aff7c 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ services: proxy: image: ghcr.io/mvdschee/reverse-proxy:latest restart: unless-stopped + # host ports 80/443 mapped to container ports 8080/8443 + # see ## Under the hood for explanation ports: - "80:8080" - "443:8443" @@ -164,7 +166,7 @@ I'll be upfront on every public project about what was done with AI. For this on - Research and tradeoff discussions - Cleanup of the README and other prose -- Talking through code-level solutions +- Discussing code-level solutions - Generating the Docker image scaffolding from a spec # TODO: diff --git a/docker-compose.yml b/docker-compose.yml index dec2d7b..a289748 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: context: . dockerfile: .docker/Dockerfile ports: + # nonroot user can't bind to ports < 1024, so map to 8080/8443 - "80:8080" - "443:8443" # Lets upstream = "host.docker.internal:" reach apps running From c208fd965edf43ff6147a9d69d401e87aa0d54b5 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:59:33 +0200 Subject: [PATCH 04/15] cleanup for acme cert --- Cargo.lock | 50 +++++--------------------------------- Cargo.toml | 9 +++++-- src/core/handlers/certs.rs | 10 +++----- src/core/setup.rs | 4 +-- 4 files changed, 18 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c080ed6..33592fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,29 +196,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "untrusted 0.7.1", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -713,12 +690,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.15.0" @@ -1036,9 +1007,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1194,7 +1165,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f05ad37c421b962354c358d347d4a6130151df9407978372d3ad7f0c8f71a64" dependencies = [ "async-trait", - "aws-lc-rs", "base64", "bytes", "http", @@ -1205,6 +1175,7 @@ dependencies = [ "hyper-rustls", "hyper-util", "rcgen", + "ring", "rustls", "rustls-pki-types", "serde", @@ -2033,7 +2004,6 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ - "aws-lc-rs", "pem", "ring", "rustls-pki-types", @@ -2090,7 +2060,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -2159,8 +2129,8 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2221,10 +2191,9 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -2771,12 +2740,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -3083,7 +3046,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs", - "aws-lc-rs", "data-encoding", "der-parser", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 6953af3..7755965 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,12 @@ arc-swap = "1.9.1" # used Ring as backend for default rcgen = { version = "0.14.8", features = ["default"] } # match the backed from rcgen -instant-acme = { version = "0.8.5", features = ["rcgen"] } +instant-acme = { version = "0.8.5", default-features = false, features = [ + "ring", + "rcgen", + "hyper-rustls", +] } + # TOML toml = "1.1.2" @@ -42,5 +47,5 @@ thiserror = "2.0.18" chrono = { version = "0.4.45", features = ["serde"] } # HTTP -http = "1.4.1" +http = "1.4.2" pingora = { version = "0.8.1", features = ["proxy", "boringssl"] } diff --git a/src/core/handlers/certs.rs b/src/core/handlers/certs.rs index 6139485..7dd81ce 100644 --- a/src/core/handlers/certs.rs +++ b/src/core/handlers/certs.rs @@ -24,7 +24,7 @@ use rcgen::{CertifiedKey, generate_simple_self_signed}; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::time; -pub fn create_initial_certs(certificate_configs: &Vec) -> Result<()> { +pub fn create_self_signed_certs(certificate_configs: &Vec) -> Result<()> { for config in certificate_configs { match config.cert_type { CertificateType::SelfSigned => { @@ -33,12 +33,7 @@ pub fn create_initial_certs(certificate_configs: &Vec) -> Res // for selfsigned we will create the certs here right away create_self_signed_certificate_files(config); }, - CertificateType::Acme => { - // we do not generate any certs here that is for the backend process, - // we are only displaying the DNS entry that needs to be included to make sure you are able to get a cert - create_acme_dns_challenge(config); - }, - CertificateType::None => {}, + _ => {}, } } @@ -121,6 +116,7 @@ impl BackgroundService for CertBackgroundRenewal { // - renew a certificate if its needed, and update the tls_store with the new certificate data // - check if the dns has its text entry so we can start the process of generating certificates for that domein // + info!("background thing"); tokio::select! { _ = tokio::time::sleep(Duration::from_secs(*self.task_interval)) => {} diff --git a/src/core/setup.rs b/src/core/setup.rs index 1ed4ab8..c8eebe1 100644 --- a/src/core/setup.rs +++ b/src/core/setup.rs @@ -3,7 +3,7 @@ use crate::{ core::{ handlers::{ certs::{ - CertBackgroundRenewal, certificate_paths, create_initial_certs, load_tls_store, + CertBackgroundRenewal, certificate_paths, create_self_signed_certs, load_tls_store, }, filesystem::{check_file_exists, safe_path}, proxy::run_proxy, @@ -75,7 +75,7 @@ impl HandleCertificates { } pub fn run(self) -> Result<(TlsStore, CertBackgroundRenewal)> { - create_initial_certs(&self.certificate_configs)?; + create_self_signed_certs(&self.certificate_configs)?; let store = load_tls_store(&self.certificate_configs)?; let renewal = CertBackgroundRenewal::new( From c4798ba6646e22b2f824fd0d76a0be94f7f048c3 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:12:52 +0200 Subject: [PATCH 05/15] WIP background acme cert creation --- Cargo.lock | 36 ++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 +++ example/example.toml | 2 +- src/config.rs | 4 ++-- src/core/handlers/certs.rs | 38 ++++++++++++++++++++++++++++++++------ src/core/models/certs.rs | 1 - src/core/setup.rs | 4 +++- src/services/certs/acme.rs | 28 +++++++++++++++++++++++++++- 8 files changed, 102 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33592fb..672bcb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -690,6 +712,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -1918,6 +1946,7 @@ dependencies = [ "instant-acme", "pingora", "rcgen", + "rustls", "serde", "thiserror 2.0.18", "tokio", @@ -2129,6 +2158,8 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -2191,6 +2222,7 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2293,9 +2325,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 7755965..d648627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ instant-acme = { version = "0.8.5", default-features = false, features = [ "rcgen", "hyper-rustls", ] } +rustls = "0.23.40" # TOML @@ -41,6 +42,8 @@ toml = "1.1.2" # Json serde = { version = "1.0.228", features = ["derive"] } +# only needed for testing +# serde_json = { version = "1.0.150" } # Others thiserror = "2.0.18" diff --git a/example/example.toml b/example/example.toml index 69e6f8a..0da207d 100644 --- a/example/example.toml +++ b/example/example.toml @@ -1,5 +1,5 @@ [acme] -email = "max@example.com" +email = "max@example.com" # use a real email this will fail the even on local testing [[routes]] host = "git.example.com" diff --git a/src/config.rs b/src/config.rs index 6a6ddde..efe2013 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,8 +16,8 @@ const HTTP_PORT_ENV: &str = "HTTP_PORT"; const HTTPS_PORT_ENV: &str = "HTTPS_PORT"; const CERT_DIR_DEFAULT: &str = ".certs/"; -const HTTP_PORT_DEFAULT: u16 = 80; -const HTTPS_PORT_DEFAULT: u16 = 443; +const HTTP_PORT_DEFAULT: u16 = 880; +const HTTPS_PORT_DEFAULT: u16 = 8443; const INPUT_ADDRESS: &str = "0.0.0.0"; // in seconds diff --git a/src/core/handlers/certs.rs b/src/core/handlers/certs.rs index 7dd81ce..359fbd2 100644 --- a/src/core/handlers/certs.rs +++ b/src/core/handlers/certs.rs @@ -4,7 +4,7 @@ use crate::{ handlers::filesystem::{check_file_exists, read_file, safe_path, write_file}, models::{ certs::{ - CertDir, CertPath, CertificateConfig, CertificateType, KeyPath, TlsMaterial, + CertDir, CertPath, CertificateConfig, CertificateType, Email, KeyPath, TlsMaterial, TlsStore, }, routes::Host, @@ -13,7 +13,8 @@ use crate::{ }, info, services::certs::{ - acme::create_acme_dns_challenge, self_signed::create_self_signed_certificate_files, + acme::{create_account, create_acme_dns_challenge}, + self_signed::create_self_signed_certificate_files, }, warn, }; @@ -92,6 +93,7 @@ pub struct CertBackgroundRenewal { pub certificate_configs: Vec, pub task_interval: TaskInterval, pub tls_store: TlsStore, + pub email: Email, } impl CertBackgroundRenewal { @@ -99,11 +101,13 @@ impl CertBackgroundRenewal { certificate_configs: Vec, task_interval: TaskInterval, tls_store: TlsStore, + email: Email, ) -> Self { Self { certificate_configs, task_interval, tls_store, + email, } } } @@ -111,11 +115,33 @@ impl CertBackgroundRenewal { #[async_trait] impl BackgroundService for CertBackgroundRenewal { async fn start(&self, mut shutdown: ShutdownWatch) { + // TODO what to do when creating an account fails (acme endpoints is 500 etc..) + let account = create_account(&self.email).await; + + let configs = self + .certificate_configs + .clone() + .into_iter() + .filter(|c| c.cert_type == CertificateType::Acme); + loop { - // TODO: couple things we will do in this background task - // - renew a certificate if its needed, and update the tls_store with the new certificate data - // - check if the dns has its text entry so we can start the process of generating certificates for that domein - // + for config in configs.clone() { + // check if we have an order open or need to create a new one + // + // if so verify the dns records with cloudflare + // + // if its set allow the order to be proccessed + // + // write to the file system + // + // swap the file content in the store with the new values if any + // + // + // note: we write to the file system so we can pick the files up and load them in the store when we restart or bootup + // this so we don't have to deal here with loading if the files are there (so we only have to check here if the order is invalid or valid and swap when its time) + // so on boot we load all the tls certs from self-signed / acme and check in this flow it its valid or not and fix it with a swap. + } + info!("background thing"); tokio::select! { diff --git a/src/core/models/certs.rs b/src/core/models/certs.rs index a0546b6..e7eaa6d 100644 --- a/src/core/models/certs.rs +++ b/src/core/models/certs.rs @@ -23,7 +23,6 @@ pub enum CertificateType { pub struct CertificateConfig { pub host: Host, pub cert_dir: CertDir, - pub email: Email, pub cert_type: CertificateType, } diff --git a/src/core/setup.rs b/src/core/setup.rs index c8eebe1..6f74afd 100644 --- a/src/core/setup.rs +++ b/src/core/setup.rs @@ -49,6 +49,7 @@ impl HandleFileSystem { pub struct HandleCertificates { certificate_configs: Vec, task_interval: TaskInterval, + email: Email, } impl HandleCertificates { @@ -63,7 +64,6 @@ impl HandleCertificates { .map(|route| CertificateConfig { host: route.host.clone(), cert_dir: cert_dir.clone(), - email: email.clone(), cert_type: route.cert_type.clone(), }) .collect::>(); @@ -71,6 +71,7 @@ impl HandleCertificates { Self { certificate_configs, task_interval, + email, } } @@ -82,6 +83,7 @@ impl HandleCertificates { self.certificate_configs.clone(), self.task_interval.clone(), store.clone(), + self.email.clone(), ); Ok((store, renewal)) diff --git a/src/services/certs/acme.rs b/src/services/certs/acme.rs index 87a9742..83a45a6 100644 --- a/src/services/certs/acme.rs +++ b/src/services/certs/acme.rs @@ -1,7 +1,33 @@ -use crate::{Error, Result, core::models::certs::CertificateConfig, info}; +use crate::{ + Error, Result, + core::models::certs::{CertificateConfig, Email}, + info, +}; +use instant_acme::{Account, AccountCredentials, LetsEncrypt, NewAccount}; +use rustls::crypto::CryptoProvider; pub fn create_acme_dns_challenge(config: &CertificateConfig) -> Result<()> { info!("creating acme dns challenge for {}", config.host); Ok(()) } + +pub async fn create_account(email: &Email) -> Result<(Account, AccountCredentials)> { + CryptoProvider::install_default(rustls::crypto::ring::default_provider()); + + let (account, credentials) = Account::builder() + .map_err(|e| Error::Certificate(e.to_string()))? + .create( + &NewAccount { + contact: &[&format!("mailto:{}", email.as_str())], + terms_of_service_agreed: true, + only_return_existing: false, + }, + LetsEncrypt::Staging.url().to_owned(), + None, + ) + .await + .map_err(|e| Error::Certificate(e.to_string()))?; + + Ok((account, credentials)) +} From a880376782d14688b9afe1e2daa892f7a99dde16 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:00:45 +0200 Subject: [PATCH 06/15] set back the port incase I forget --- src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index efe2013..6a6ddde 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,8 +16,8 @@ const HTTP_PORT_ENV: &str = "HTTP_PORT"; const HTTPS_PORT_ENV: &str = "HTTPS_PORT"; const CERT_DIR_DEFAULT: &str = ".certs/"; -const HTTP_PORT_DEFAULT: u16 = 880; -const HTTPS_PORT_DEFAULT: u16 = 8443; +const HTTP_PORT_DEFAULT: u16 = 80; +const HTTPS_PORT_DEFAULT: u16 = 443; const INPUT_ADDRESS: &str = "0.0.0.0"; // in seconds From bd14719edb50442176dfabb62aa44442237c0d6f Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:44:55 +0200 Subject: [PATCH 07/15] added order creation and WIP on dns provider services --- .gitignore | 1 + Cargo.lock | 4 ++-- Cargo.toml | 2 +- makefile | 2 +- rust-toolchain.toml | 2 +- src/config.rs | 4 ++-- src/core/handlers/certs.rs | 42 +++++++++++++++++++++++++++++++++----- src/error.rs | 3 +++ src/services/certs/acme.rs | 35 +++++++++++++++++++++---------- src/services/dns/mod.rs | 0 10 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 src/services/dns/mod.rs diff --git a/.gitignore b/.gitignore index fb68da6..dfb8b15 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target/ .env .DS_Store .certs/ +example/local.toml diff --git a/Cargo.lock b/Cargo.lock index 672bcb7..834f359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2154,9 +2154,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "log", diff --git a/Cargo.toml b/Cargo.toml index d648627..aca6c91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ instant-acme = { version = "0.8.5", default-features = false, features = [ "rcgen", "hyper-rustls", ] } -rustls = "0.23.40" +rustls = "0.23.41" # TOML diff --git a/makefile b/makefile index 3cd59f6..b0e05da 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ dev: - CONFIG_PATH=./example/example.toml watchexec -q -c -w src --exts rs --restart "cargo run" + CONFIG_PATH=./example/local.toml watchexec -q -c -w src --exts rs --restart "cargo run" scan: foxguard --config .foxguard.yml diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 554f234..03e6777 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.90.0" +channel = "1.96.0" components = ["clippy", "rustfmt", "rust-analyzer"] diff --git a/src/config.rs b/src/config.rs index 6a6ddde..3aa71ae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,8 +20,8 @@ const HTTP_PORT_DEFAULT: u16 = 80; const HTTPS_PORT_DEFAULT: u16 = 443; const INPUT_ADDRESS: &str = "0.0.0.0"; -// in seconds -const CERT_BACKGROUND_TASK_INTERVAL: u64 = 120; +/// in seconds +const CERT_BACKGROUND_TASK_INTERVAL: u64 = 3600; // 1 hour #[derive(Debug, Clone)] pub struct Config { diff --git a/src/core/handlers/certs.rs b/src/core/handlers/certs.rs index 359fbd2..c28ffc9 100644 --- a/src/core/handlers/certs.rs +++ b/src/core/handlers/certs.rs @@ -11,15 +11,16 @@ use crate::{ tasks::TaskInterval, }, }, - info, + error, info, services::certs::{ - acme::{create_account, create_acme_dns_challenge}, + acme::{create_account, create_order}, self_signed::create_self_signed_certificate_files, }, warn, }; use arc_swap::ArcSwap; use async_trait::async_trait; +use instant_acme::{Identifier, NewOrder, OrderStatus}; use pingora::{server::ShutdownWatch, services::background::BackgroundService, tls}; use rcgen::{CertifiedKey, generate_simple_self_signed}; use std::{collections::HashMap, sync::Arc, time::Duration}; @@ -53,7 +54,8 @@ pub fn load_tls_store(certificate_configs: &Vec) -> Result pair, + Err(ref err) => { + error!("{err:?}"); + continue; + }, + }; + for config in configs.clone() { - // check if we have an order open or need to create a new one + let order_result = create_order(&account, &config.host).await; + + let mut order = match order_result { + Ok(order) => order, + Err(err) => { + error!("{err:?}"); + continue; + }, + }; + + let state = order.state(); + info!("order state: {:#?}", state); + + if !matches!(state.status, OrderStatus::Pending) { + warn!("Skipping non-Pending order: {:?}", state.status); + continue; + } + // // if so verify the dns records with cloudflare // diff --git a/src/error.rs b/src/error.rs index a9f79ac..e1e4da8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,6 +19,9 @@ pub enum Error { #[error("certificate error: {0}")] Certificate(String), + #[error("Acme error: {0}")] + Acme(String), + #[error("proxy error: {0}")] Proxy(String), diff --git a/src/services/certs/acme.rs b/src/services/certs/acme.rs index 83a45a6..f6aaea5 100644 --- a/src/services/certs/acme.rs +++ b/src/services/certs/acme.rs @@ -1,33 +1,46 @@ use crate::{ Error, Result, - core::models::certs::{CertificateConfig, Email}, - info, + core::models::{ + certs::{CertificateConfig, Email}, + routes::Host, + }, +}; +use instant_acme::{ + Account, AccountCredentials, Identifier, LetsEncrypt, NewAccount, NewOrder, Order, }; -use instant_acme::{Account, AccountCredentials, LetsEncrypt, NewAccount}; use rustls::crypto::CryptoProvider; -pub fn create_acme_dns_challenge(config: &CertificateConfig) -> Result<()> { - info!("creating acme dns challenge for {}", config.host); - - Ok(()) -} - pub async fn create_account(email: &Email) -> Result<(Account, AccountCredentials)> { CryptoProvider::install_default(rustls::crypto::ring::default_provider()); let (account, credentials) = Account::builder() - .map_err(|e| Error::Certificate(e.to_string()))? + .map_err(|e| Error::Acme(e.to_string()))? .create( &NewAccount { contact: &[&format!("mailto:{}", email.as_str())], terms_of_service_agreed: true, only_return_existing: false, }, + // TODO: switch to prod LetsEncrypt::Staging.url().to_owned(), None, ) .await - .map_err(|e| Error::Certificate(e.to_string()))?; + .map_err(|e| Error::Acme(e.to_string()))?; Ok((account, credentials)) } + +pub async fn create_order(account: &Account, host: &Host) -> Result { + let identifier = Identifier::Dns(host.to_string()); + // instant_acme support multiple host per order, + // but we need to know which dns needs to be updated + // which is why we split them up, this is maybe something we can refine in V2 + let identifiers = vec![identifier]; + let order_result = account + .new_order(&NewOrder::new(&identifiers)) + .await + .map_err(|e| Error::Acme(format!("Failed with new_order: {}", e))); + + order_result +} diff --git a/src/services/dns/mod.rs b/src/services/dns/mod.rs new file mode 100644 index 0000000..e69de29 From 91d0f5d425d0e9e8c956e895e66afffb0e6d90d8 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Mon, 10 Aug 2026 08:23:59 +0200 Subject: [PATCH 08/15] WIP DNS providers integration --- src/core/models/dns.rs | 26 ++++++++++++++++++++++++++ src/core/models/mod.rs | 1 + src/error.rs | 3 +++ src/services/dns/mod.rs | 15 +++++++++++++++ src/services/mod.rs | 1 + 5 files changed, 46 insertions(+) create mode 100644 src/core/models/dns.rs diff --git a/src/core/models/dns.rs b/src/core/models/dns.rs new file mode 100644 index 0000000..f5792e4 --- /dev/null +++ b/src/core/models/dns.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; +use std::{fmt, ops::Deref}; + +// --- EMAIL --- +#[derive(Debug, Clone, Deserialize)] +pub struct Record(String); + +impl Deref for Record { + type Target = String; + + fn deref(&self) -> &String { + &self.0 + } +} + +impl fmt::Display for Record { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Record { + fn from(s: String) -> Self { + Record(s) + } +} diff --git a/src/core/models/mod.rs b/src/core/models/mod.rs index 1cf9ecb..7052b71 100644 --- a/src/core/models/mod.rs +++ b/src/core/models/mod.rs @@ -1,4 +1,5 @@ pub mod certs; +pub mod dns; pub mod filesystem; pub mod proxy; pub mod routes; diff --git a/src/error.rs b/src/error.rs index e1e4da8..80712fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -25,6 +25,9 @@ pub enum Error { #[error("proxy error: {0}")] Proxy(String), + #[error("dns error: {0}")] + Dns(String), + #[error("config error: {0}")] Config(String), diff --git a/src/services/dns/mod.rs b/src/services/dns/mod.rs index e69de29..cf94684 100644 --- a/src/services/dns/mod.rs +++ b/src/services/dns/mod.rs @@ -0,0 +1,15 @@ +use crate::core::models::dns::Record; +use crate::core::models::routes::Host; +use crate::{Error, Result}; + +trait DnsProvider: Send + Sync + 'static { + /// Only returns single record + /// gets all records but internally filters out all records either, + /// after the call or if it supports it directly on the endpoint call. + fn get_challenge_record(&self, host: Host) -> Result; + /// Single fuction to set and update + /// will return the set record (if returned otherwise fake it) + fn update_challenge_record(&self, host: Host) -> Result; +} + +pub struct CloudflareDns {} diff --git a/src/services/mod.rs b/src/services/mod.rs index e410b93..ad80d1c 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1 +1,2 @@ pub mod certs; +pub mod dns; From ddc5edd458493a9267eeee015a062cd8d6dc4ae6 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:43:56 +0200 Subject: [PATCH 09/15] added macro for newtype string --- src/core/models/certs.rs | 53 ++++------------------------------- src/core/models/dns.rs | 38 +++++++++++++------------ src/core/models/filesystem.rs | 25 ++--------------- src/core/models/proxy.rs | 36 ++++++------------------ src/core/models/routes.rs | 53 +++++------------------------------ src/utils.rs | 27 ++++++++++++++++++ 6 files changed, 71 insertions(+), 161 deletions(-) diff --git a/src/core/models/certs.rs b/src/core/models/certs.rs index e7eaa6d..e8ddd57 100644 --- a/src/core/models/certs.rs +++ b/src/core/models/certs.rs @@ -1,4 +1,7 @@ -use crate::core::models::{filesystem::SafePath, routes::Host}; +use crate::{ + core::models::{filesystem::SafePath, routes::Host}, + string_newtype, +}; use arc_swap::ArcSwap; use pingora::tls::{ pkey::{PKey, Private}, @@ -6,8 +9,6 @@ use pingora::tls::{ }; use serde::Deserialize; use std::collections::HashMap; -use std::fmt; -use std::ops::Deref; use std::sync::Arc; #[derive(Debug, Clone, Deserialize, Default, PartialEq)] @@ -37,49 +38,7 @@ pub type CertPath = SafePath; pub type TlsStore = Arc>>; // --- EMAIL --- -#[derive(Debug, Clone, Deserialize)] -pub struct Email(String); - -impl Deref for Email { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for Email { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for Email { - fn from(s: String) -> Self { - Email(s) - } -} +string_newtype!(Email, derive(Deserialize)); // --- CERT_DIR --- -#[derive(Debug, Clone, Deserialize)] -pub struct CertDir(String); - -impl Deref for CertDir { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for CertDir { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for CertDir { - fn from(s: String) -> Self { - CertDir(s) - } -} +string_newtype!(CertDir, derive(Deserialize)); diff --git a/src/core/models/dns.rs b/src/core/models/dns.rs index f5792e4..1c28cfc 100644 --- a/src/core/models/dns.rs +++ b/src/core/models/dns.rs @@ -1,26 +1,28 @@ +use crate::string_newtype; use serde::Deserialize; -use std::{fmt, ops::Deref}; -// --- EMAIL --- -#[derive(Debug, Clone, Deserialize)] -pub struct Record(String); +// --- DNS Record --- +string_newtype!(Record, derive(Deserialize)); -impl Deref for Record { - type Target = String; +// --- DNS Provider credentials --- - fn deref(&self) -> &String { - &self.0 - } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProviderCredentails { + Cloudflare(CloudflareProvider), } -impl fmt::Display for Record { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} +// --- add here any other providers credentials --- -impl From for Record { - fn from(s: String) -> Self { - Record(s) - } +// --- CloudFlare --- +#[derive(Debug, Clone, Deserialize)] +pub struct CloudflareProvider { + pub zone_id: ZoneId, + pub api_token: ApiToken, } + +// Zone ID +string_newtype!(ZoneId, derive(Deserialize)); + +// API token +string_newtype!(ApiToken, derive(Deserialize)); diff --git a/src/core/models/filesystem.rs b/src/core/models/filesystem.rs index 53c4570..3a70b54 100644 --- a/src/core/models/filesystem.rs +++ b/src/core/models/filesystem.rs @@ -1,25 +1,4 @@ -use std::{fmt, ops::Deref}; +use crate::string_newtype; // --- SAFEPATH --- -#[derive(Debug, Clone)] -pub struct SafePath(String); - -impl Deref for SafePath { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for SafePath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for SafePath { - fn from(s: String) -> Self { - SafePath(s) - } -} +string_newtype!(SafePath); diff --git a/src/core/models/proxy.rs b/src/core/models/proxy.rs index ca5438f..7db4914 100644 --- a/src/core/models/proxy.rs +++ b/src/core/models/proxy.rs @@ -1,10 +1,13 @@ -use crate::core::models::{ - certs::CertDir, - filesystem::SafePath, - routes::{Host, Upstream}, +use crate::{ + core::models::{ + certs::CertDir, + filesystem::SafePath, + routes::{Host, Upstream}, + }, + string_newtype, }; use http::{Response, StatusCode, header}; -use std::{collections::HashMap, fmt, ops::Deref, sync::Arc}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; pub type ProxyRouteMap = Arc>; @@ -59,25 +62,4 @@ impl From for ProxyPort { } // --- PROXY INPUT ADDRESS --- -#[derive(Debug, Clone)] -pub struct ProxyInputAddress(String); - -impl Deref for ProxyInputAddress { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for ProxyInputAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for ProxyInputAddress { - fn from(s: String) -> Self { - ProxyInputAddress(s) - } -} +string_newtype!(ProxyInputAddress); diff --git a/src/core/models/routes.rs b/src/core/models/routes.rs index 17649ec..366d319 100644 --- a/src/core/models/routes.rs +++ b/src/core/models/routes.rs @@ -1,6 +1,8 @@ -use crate::core::models::certs::CertificateType; +use crate::{ + core::models::{certs::CertificateType, dns::ProviderCredentails}, + string_newtype, +}; use serde::Deserialize; -use std::{fmt, ops::Deref}; #[derive(Debug, Clone, Deserialize)] pub struct Route { @@ -8,19 +10,11 @@ pub struct Route { pub upstream: Upstream, #[serde(default)] pub cert_type: CertificateType, + pub dns_provider: Option, } // --- HOST --- -#[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)] -pub struct Host(String); - -impl Deref for Host { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} +string_newtype!(Host, derive(Deserialize, Hash, Eq, PartialEq)); impl std::borrow::Borrow for Host { fn borrow(&self) -> &str { @@ -28,38 +22,5 @@ impl std::borrow::Borrow for Host { } } -impl fmt::Display for Host { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for Host { - fn from(s: String) -> Self { - Host(s) - } -} - // --- UPSTREAM --- -#[derive(Debug, Clone, Deserialize)] -pub struct Upstream(String); - -impl Deref for Upstream { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for Upstream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for Upstream { - fn from(s: String) -> Self { - Upstream(s) - } -} +string_newtype!(Upstream, derive(Deserialize)); diff --git a/src/utils.rs b/src/utils.rs index b253deb..4225ce5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -18,3 +18,30 @@ macro_rules! error { println!("\x1b[90m{} \x1b[31m{} \x1b[0m{}", chrono::Local::now().format("%H:%M:%S%.3f %d-%m-%y"), "[ERROR]", format_args!($($arg)*)) }; } + +#[macro_export] +macro_rules! string_newtype { + ($name:ident $(, derive($($extra:path),+ $(,)?))?) => { + #[derive(Debug, Clone $(, $($extra),+)?)] + pub struct $name(String); + + impl ::std::ops::Deref for $name { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } + } + + impl ::std::fmt::Display for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}", self.0) + } + } + + impl From for $name { + fn from(s: String) -> Self { + $name(s) + } + } + }; +} From 20be828d9ed952f7857a56d428a6ba354f4af3d1 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:51:29 +0200 Subject: [PATCH 10/15] BREAKING: set default cert type to None --- README.md | 2 +- example/example.toml | 4 ++-- src/core/models/certs.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7904482..d4d63d7 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ The full schema lives in [`example/example.toml`](example/example.toml). The fie | `acme.email` | yes | Contact email for Let's Encrypt (used once ACME lands; required today even if every route is `none`). | | `routes[].host` | yes | The `Host` header to match (e.g. `app.example.com`). | | `routes[].upstream` | yes | `host:port` to forward to. Use `host.docker.internal:` to reach the host machine from Docker. | -| `routes[].cert_type` | no | `self_signed` (default works on boot), `acme` (WIP), or `none` (HTTP only). Defaults to `acme`. | +| `routes[].cert_type` | no | `self_signed` (default works on boot), `acme` (WIP), or `none` (HTTP only). Defaults to `none`. | ## How it works diff --git a/example/example.toml b/example/example.toml index 0da207d..6eb0a62 100644 --- a/example/example.toml +++ b/example/example.toml @@ -4,9 +4,9 @@ email = "max@example.com" # use a real email this will fail the even on lo [[routes]] host = "git.example.com" upstream = "127.0.0.1:3000" # use "host.docker.internal:3000" when using docker -cert_type = "self_signed" # default is "acme" +cert_type = "self_signed" # default is "none" [[routes]] host = "test.example.com" upstream = "127.0.0.1:3000" # use "host.docker.internal:3000" when using docker -cert_type = "none" # default is "acme" +cert_type = "none" # default is "none" diff --git a/src/core/models/certs.rs b/src/core/models/certs.rs index e8ddd57..b5289cb 100644 --- a/src/core/models/certs.rs +++ b/src/core/models/certs.rs @@ -15,8 +15,8 @@ use std::sync::Arc; #[serde(rename_all = "snake_case")] pub enum CertificateType { SelfSigned, - #[default] Acme, + #[default] None, } From 447eff93953bb5cf439f754c275ccdfe8d85b489 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:25:00 +0200 Subject: [PATCH 11/15] WIP cloudflare as first dns provider --- Cargo.lock | 632 +++++++++++++++++++++++++++++++-- Cargo.toml | 15 +- src/services/dns/cloudflare.rs | 13 + src/services/dns/mod.rs | 6 +- 4 files changed, 632 insertions(+), 34 deletions(-) create mode 100644 src/services/dns/cloudflare.rs diff --git a/Cargo.lock b/Cargo.lock index 48a0ffd..8706bdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -146,7 +146,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.19", + "thiserror 2.0.20", "time", ] @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", @@ -426,6 +426,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -516,6 +533,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -532,6 +559,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -724,6 +760,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -802,6 +847,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -923,8 +977,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -935,10 +991,24 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + [[package]] name = "getset" version = "0.1.6" @@ -1035,9 +1105,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1122,18 +1192,23 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1160,12 +1235,115 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1208,10 +1386,16 @@ dependencies = [ "rustls-pki-types", "serde", "serde_json", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", ] +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1245,7 +1429,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -1336,6 +1520,12 @@ dependencies = [ "libc", ] +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "local-ip-address" version = "0.6.12" @@ -1371,6 +1561,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.8.0" @@ -1386,6 +1582,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1868,6 +2070,15 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1946,13 +2157,71 @@ dependencies = [ "instant-acme", "pingora", "rcgen", + "reqwest", "rustls", "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "toml", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -1968,6 +2237,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.6" @@ -1989,6 +2264,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2027,6 +2313,21 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rcgen" version = "0.14.8" @@ -2079,6 +2380,44 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2154,9 +2493,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -2186,6 +2525,7 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ + "web-time", "zeroize", ] @@ -2195,7 +2535,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -2271,7 +2611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2429,6 +2769,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -2496,6 +2842,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2507,6 +2862,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2518,11 +2894,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -2538,9 +2914,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", @@ -2597,6 +2973,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.53.1" @@ -2672,9 +3073,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -2696,9 +3097,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] @@ -2709,6 +3110,45 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -2789,6 +3229,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2848,6 +3306,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.121" @@ -2880,6 +3348,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-root-certs" version = "1.0.7" @@ -2961,6 +3449,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -3082,6 +3581,12 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "x509-parser" version = "0.18.1" @@ -3096,7 +3601,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.19", + "thiserror 2.0.20", "time", ] @@ -3110,6 +3615,29 @@ dependencies = [ "time", ] +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -3130,12 +3658,66 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index f04283e..607b0af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,11 @@ unused = { level = "allow", priority = -1 } [dependencies] # Async -async-trait = "0.1.91" +async-trait = "0.1.92" tokio = { version = "1.53.1", features = ["full"] } # Arc -arc-swap = "1.9.1" +arc-swap = "1.9.2" # Certs # used Ring as backend for default @@ -34,19 +34,22 @@ instant-acme = { version = "0.8.5", default-features = false, features = [ "rcgen", "hyper-rustls", ] } -rustls = "0.23.41" +rustls = "0.23.43" # TOML -toml = "1.1.3" +toml = "1.1.4" # Json serde = { version = "1.0.229", features = ["derive"] } # Others -thiserror = "2.0.19" +thiserror = "2.0.20" chrono = { version = "0.4.45", features = ["serde"] } # HTTP -http = "1.4.2" +http = "1.5.0" pingora = { version = "0.8.1", features = ["proxy", "boringssl"] } + +# HTTP Client +reqwest = "0.13.4" diff --git a/src/services/dns/cloudflare.rs b/src/services/dns/cloudflare.rs new file mode 100644 index 0000000..074dfbf --- /dev/null +++ b/src/services/dns/cloudflare.rs @@ -0,0 +1,13 @@ +use crate::core::models::dns::{CloudflareProvider, Record}; +use crate::core::models::routes::Host; +use crate::services::dns::DnsProvider; +use crate::{Error, Result}; + +impl DnsProvider for CloudflareProvider { + fn get_challenge_record(&self, host: Host) -> Result { + Err(Error::Dns("not implemented!".to_string())) + } + fn update_challenge_record(&self, host: Host) -> Result { + Err(Error::Dns("not implemented!".to_string())) + } +} diff --git a/src/services/dns/mod.rs b/src/services/dns/mod.rs index cf94684..eb889d7 100644 --- a/src/services/dns/mod.rs +++ b/src/services/dns/mod.rs @@ -1,7 +1,9 @@ -use crate::core::models::dns::Record; +use crate::core::models::dns::{CloudflareProvider, Record}; use crate::core::models::routes::Host; use crate::{Error, Result}; +mod cloudflare; + trait DnsProvider: Send + Sync + 'static { /// Only returns single record /// gets all records but internally filters out all records either, @@ -11,5 +13,3 @@ trait DnsProvider: Send + Sync + 'static { /// will return the set record (if returned otherwise fake it) fn update_challenge_record(&self, host: Host) -> Result; } - -pub struct CloudflareDns {} From 7607e526e2ebbe0afeda1b891eafa12bac20f708 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Thu, 27 Aug 2026 08:39:05 +0200 Subject: [PATCH 12/15] reworking acme account creation --- Cargo.lock | 2 + Cargo.toml | 2 +- src/config.rs | 10 +++- src/core/handlers/certs.rs | 92 +++++++++++++++++++++++++++++++--- src/core/models/certs.rs | 6 ++- src/core/models/dns.rs | 24 ++++++++- src/core/models/routes.rs | 4 +- src/core/setup.rs | 1 + src/error.rs | 9 ++++ src/services/certs/acme.rs | 13 +++++ src/services/dns/cloudflare.rs | 9 ++-- src/services/dns/mod.rs | 14 ------ src/services/http/mod.rs | 15 ++++++ src/services/mod.rs | 1 + 14 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 src/services/http/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8706bdf..ef5bd9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2406,6 +2406,8 @@ dependencies = [ "rustls", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper", "tokio", "tokio-rustls", diff --git a/Cargo.toml b/Cargo.toml index 607b0af..03ab67c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,4 +52,4 @@ http = "1.5.0" pingora = { version = "0.8.1", features = ["proxy", "boringssl"] } # HTTP Client -reqwest = "0.13.4" +reqwest = { version = "0.13.4", features = ["json"] } diff --git a/src/config.rs b/src/config.rs index 3aa71ae..9f33640 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,13 @@ use crate::{ Error, Result, core::models::{ - certs::{CertDir, Email}, + certs::{CertAccountPath, CertDir, Email}, proxy::{ProxyInputAddress, ProxyPort}, routes::Route, tasks::TaskInterval, }, }; +use instant_acme::AccountCredentials; use serde::Deserialize; use std::{env, fs}; @@ -16,6 +17,8 @@ const HTTP_PORT_ENV: &str = "HTTP_PORT"; const HTTPS_PORT_ENV: &str = "HTTPS_PORT"; const CERT_DIR_DEFAULT: &str = ".certs/"; +// this will be stored in the .certs/ or depending on where the user wants to store it +const CERT_CREDENTIAL_FILE: &str = "acme_account"; const HTTP_PORT_DEFAULT: u16 = 80; const HTTPS_PORT_DEFAULT: u16 = 443; const INPUT_ADDRESS: &str = "0.0.0.0"; @@ -27,6 +30,8 @@ const CERT_BACKGROUND_TASK_INTERVAL: u64 = 3600; // 1 hour pub struct Config { pub email: Email, pub cert_dir: CertDir, + // opague string type as it can't be cloned when its in AccountCredentials type + pub cert_account_path: CertAccountPath, pub routes: Vec, pub task_interval: TaskInterval, pub http_port: ProxyPort, @@ -51,6 +56,8 @@ impl Config { let config_file = parse_toml_config(config_path)?; let cert_dir = load_env(CERT_DIR_ENV).unwrap_or_else(|_| CERT_DIR_DEFAULT.to_string()); + let cert_account_path = format!("{}/{}", cert_dir, CERT_CREDENTIAL_FILE); + let http_port = load_env(HTTP_PORT_ENV).ok().and_then(|v| v.parse().ok()).unwrap_or(HTTP_PORT_DEFAULT); let https_port = load_env(HTTPS_PORT_ENV) @@ -61,6 +68,7 @@ impl Config { Ok(Config { email: config_file.acme.email.clone(), cert_dir: CertDir::from(cert_dir), + cert_account_path: CertAccountPath::from(cert_account_path), routes: config_file.routes.clone(), task_interval: TaskInterval::from(CERT_BACKGROUND_TASK_INTERVAL), http_port: ProxyPort::from(http_port), diff --git a/src/core/handlers/certs.rs b/src/core/handlers/certs.rs index c28ffc9..624e1d3 100644 --- a/src/core/handlers/certs.rs +++ b/src/core/handlers/certs.rs @@ -7,14 +7,18 @@ use crate::{ CertDir, CertPath, CertificateConfig, CertificateType, Email, KeyPath, TlsMaterial, TlsStore, }, + dns::{Cloudflare, CloudflareProvider, DnsProvider, ProviderCredentail}, routes::Host, tasks::TaskInterval, }, }, error, info, - services::certs::{ - acme::{create_account, create_order}, - self_signed::create_self_signed_certificate_files, + services::{ + certs::{ + acme::{create_account, create_order}, + self_signed::create_self_signed_certificate_files, + }, + http::create_client, }, warn, }; @@ -120,6 +124,14 @@ impl BackgroundService for CertBackgroundRenewal { // this means we have to be a little more verbose with our error handeling. // TLDR; just continue on any error :D, problem for the next loop :') async fn start(&self, mut shutdown: ShutdownWatch) { + let http_client = match create_client() { + Ok(client) => client, + Err(e) => { + error!("Can't start cert renewal loop: {}", e); + return; + }, + }; + // TODO what to do when creating an account fails (acme endpoints is 500 etc..) let account_result = create_account(&self.email) .await @@ -143,6 +155,18 @@ impl BackgroundService for CertBackgroundRenewal { for config in configs.clone() { let order_result = create_order(&account, &config.host).await; + // if no DNS credentials are provided there is DNS validation. + // we should terminate early on. + let dns_service_config = match config.provider_config { + Some(config) => config, + None => { + warn!("No DNS credentials provided for {}", config.host); + continue; + }, + }; + + let dns_service = get_dns_services(dns_service_config, "_acme-challenge."); + let mut order = match order_result { Ok(order) => order, Err(err) => { @@ -154,16 +178,58 @@ impl BackgroundService for CertBackgroundRenewal { let state = order.state(); info!("order state: {:#?}", state); + // TODO what does pending means? can we use this to gate the refresh on it. like this will tell us its time to refresh the dns record. if !matches!(state.status, OrderStatus::Pending) { warn!("Skipping non-Pending order: {:?}", state.status); continue; } - // - // if so verify the dns records with cloudflare - // - // if its set allow the order to be proccessed - // + // TODO dont verify the value go straigh to update we are going to get get_challenge_record once we have put it and use it as a input check before finalizing + + // Pick the desired challenge type and prepare the response. + + // let mut authorizations = order.authorizations(); + // while let Some(result) = authorizations.next().await { + // let mut authz = result?; + // match authz.status { + // AuthorizationStatus::Pending => {}, + // AuthorizationStatus::Valid => continue, + // _ => todo!(), + // } + + // // We'll use the DNS challenges for this example, but you could + // // pick something else to use here. + + // let mut challenge = authz + // .challenge(ChallengeType::Dns01) + // .ok_or_else(|| anyhow::anyhow!("no dns01 challenge found"))?; + + // println!("Please set the following DNS record then press the Return key:"); + // println!( + // "_acme-challenge.{} IN TXT {}", + // challenge.identifier(), + // challenge.key_authorization()?.dns_value() + // ); + // io::stdin().read_line(&mut String::new())?; + + // challenge.set_ready().await?; + // } + + // // Exponentially back off until the order becomes ready or invalid. + + // let status = order.poll_ready(&RetryPolicy::default()).await?; + // if status != OrderStatus::Ready { + // return Err(anyhow::anyhow!("unexpected order status: {status:?}")); + // } + + // // Finalize the order and print certificate chain, private key and account credentials. + + // let private_key_pem = order.finalize().await?; + // let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?; + + // info!("certificate chain:\n\n{cert_chain_pem}"); + // info!("private key:\n\n{private_key_pem}"); + // write to the file system // // swap the file content in the store with the new values if any @@ -183,3 +249,13 @@ impl BackgroundService for CertBackgroundRenewal { } } } + +fn get_dns_services(config: ProviderCredentail, challenge_prefix: &str) -> impl DnsProvider { + match config { + ProviderCredentail::Cloudflare(config) => CloudflareProvider { + zone_id: config.zone_id, + api_token: config.api_token, + challenge_prefix: challenge_prefix.to_string(), + }, + } +} diff --git a/src/core/models/certs.rs b/src/core/models/certs.rs index b5289cb..a498a1a 100644 --- a/src/core/models/certs.rs +++ b/src/core/models/certs.rs @@ -1,5 +1,5 @@ use crate::{ - core::models::{filesystem::SafePath, routes::Host}, + core::models::{dns::ProviderCredentail, filesystem::SafePath, routes::Host}, string_newtype, }; use arc_swap::ArcSwap; @@ -25,6 +25,7 @@ pub struct CertificateConfig { pub host: Host, pub cert_dir: CertDir, pub cert_type: CertificateType, + pub provider_config: Option, } pub struct TlsMaterial { @@ -42,3 +43,6 @@ string_newtype!(Email, derive(Deserialize)); // --- CERT_DIR --- string_newtype!(CertDir, derive(Deserialize)); + +// --- CERT_ACCOUNT_PATH --- +string_newtype!(CertAccountPath, derive(Deserialize)); diff --git a/src/core/models/dns.rs b/src/core/models/dns.rs index 1c28cfc..75c4ebe 100644 --- a/src/core/models/dns.rs +++ b/src/core/models/dns.rs @@ -1,6 +1,19 @@ +use crate::core::models::routes::Host; use crate::string_newtype; +use crate::{Error, Result}; +use reqwest::Client; use serde::Deserialize; +pub trait DnsProvider: Send + Sync + 'static { + /// Only returns single record + /// gets all records but internally filters out all records either, + /// after the call or if it supports it directly on the endpoint call. + fn get_challenge_record(&self, host: &Host) -> Result; + /// Single fuction to set and update + /// will return the set record (if returned otherwise fake it) + fn update_challenge_record(&self, host: &Host, value: &str) -> Result; +} + // --- DNS Record --- string_newtype!(Record, derive(Deserialize)); @@ -8,17 +21,26 @@ string_newtype!(Record, derive(Deserialize)); #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum ProviderCredentails { +pub enum ProviderCredentail { Cloudflare(CloudflareProvider), } // --- add here any other providers credentials --- // --- CloudFlare --- + +// config coming from TOML file #[derive(Debug, Clone, Deserialize)] pub struct CloudflareProvider { pub zone_id: ZoneId, pub api_token: ApiToken, + pub challenge_prefix: String, +} + +// struct that hold client +pub struct Cloudflare { + client: Client, + config: CloudflareProvider, } // Zone ID diff --git a/src/core/models/routes.rs b/src/core/models/routes.rs index 366d319..b44a2e3 100644 --- a/src/core/models/routes.rs +++ b/src/core/models/routes.rs @@ -1,5 +1,5 @@ use crate::{ - core::models::{certs::CertificateType, dns::ProviderCredentails}, + core::models::{certs::CertificateType, dns::ProviderCredentail}, string_newtype, }; use serde::Deserialize; @@ -10,7 +10,7 @@ pub struct Route { pub upstream: Upstream, #[serde(default)] pub cert_type: CertificateType, - pub dns_provider: Option, + pub dns_provider: Option, } // --- HOST --- diff --git a/src/core/setup.rs b/src/core/setup.rs index 6f74afd..2da6167 100644 --- a/src/core/setup.rs +++ b/src/core/setup.rs @@ -65,6 +65,7 @@ impl HandleCertificates { host: route.host.clone(), cert_dir: cert_dir.clone(), cert_type: route.cert_type.clone(), + provider_config: route.dns_provider.clone(), }) .collect::>(); diff --git a/src/error.rs b/src/error.rs index 80712fa..10da973 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,9 @@ pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error), + #[error("request error: {0}")] + Request(reqwest::Error), + #[error("file system error: {0}")] FileSystem(String), @@ -34,3 +37,9 @@ pub enum Error { #[error("unknown error")] Unknown, } + +impl From for Error { + fn from(err: reqwest::Error) -> Self { + Error::Request(err) + } +} diff --git a/src/services/certs/acme.rs b/src/services/certs/acme.rs index f6aaea5..99c7c05 100644 --- a/src/services/certs/acme.rs +++ b/src/services/certs/acme.rs @@ -10,6 +10,19 @@ use instant_acme::{ }; use rustls::crypto::CryptoProvider; +// w +pub async fn load_account(credentials: AccountCredentials) -> Result { + let account = Account::builder() + .map_err(|e| Error::Acme(e.to_string()))? + .from_credentials(credentials) + .await + .map_err(|e| Error::Acme(e.to_string()))?; + + Ok(account) +} + +// this one returns credentials, because we are going to save it to the file +// for later usage to start the process with the same credentials pub async fn create_account(email: &Email) -> Result<(Account, AccountCredentials)> { CryptoProvider::install_default(rustls::crypto::ring::default_provider()); diff --git a/src/services/dns/cloudflare.rs b/src/services/dns/cloudflare.rs index 074dfbf..7bb4ee8 100644 --- a/src/services/dns/cloudflare.rs +++ b/src/services/dns/cloudflare.rs @@ -1,13 +1,14 @@ -use crate::core::models::dns::{CloudflareProvider, Record}; +use serde::de::value; + +use crate::core::models::dns::{CloudflareProvider, DnsProvider, Record}; use crate::core::models::routes::Host; -use crate::services::dns::DnsProvider; use crate::{Error, Result}; impl DnsProvider for CloudflareProvider { - fn get_challenge_record(&self, host: Host) -> Result { + fn get_challenge_record(&self, host: &Host) -> Result { Err(Error::Dns("not implemented!".to_string())) } - fn update_challenge_record(&self, host: Host) -> Result { + fn update_challenge_record(&self, host: &Host, value: &str) -> Result { Err(Error::Dns("not implemented!".to_string())) } } diff --git a/src/services/dns/mod.rs b/src/services/dns/mod.rs index eb889d7..c727ae5 100644 --- a/src/services/dns/mod.rs +++ b/src/services/dns/mod.rs @@ -1,15 +1 @@ -use crate::core::models::dns::{CloudflareProvider, Record}; -use crate::core::models::routes::Host; -use crate::{Error, Result}; - mod cloudflare; - -trait DnsProvider: Send + Sync + 'static { - /// Only returns single record - /// gets all records but internally filters out all records either, - /// after the call or if it supports it directly on the endpoint call. - fn get_challenge_record(&self, host: Host) -> Result; - /// Single fuction to set and update - /// will return the set record (if returned otherwise fake it) - fn update_challenge_record(&self, host: Host) -> Result; -} diff --git a/src/services/http/mod.rs b/src/services/http/mod.rs new file mode 100644 index 0000000..731029c --- /dev/null +++ b/src/services/http/mod.rs @@ -0,0 +1,15 @@ +use crate::Result; +use reqwest::Client; +use std::time::Duration; + +// simply return client no need for struct, +// or extras +pub fn create_client() -> Result { + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .pool_idle_timeout(Duration::from_secs(90)) + .pool_max_idle_per_host(10) + .build()?; + + Ok(client) +} diff --git a/src/services/mod.rs b/src/services/mod.rs index ad80d1c..cc7959b 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,2 +1,3 @@ pub mod certs; pub mod dns; +pub mod http; From a12aecf6ee49ac6f4ed4c7e2094789b3a43e0e0d Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:25:44 +0200 Subject: [PATCH 13/15] fix loading acme account to prevent duplicated accounts creation --- .rustfmt.toml | 3 ++ Cargo.lock | 5 +-- Cargo.toml | 1 + src/config.rs | 5 +-- src/core/handlers/certs.rs | 65 ++++++++++++++++++++++++++------------ src/core/models/certs.rs | 2 +- src/core/models/dns.rs | 10 ++++++ src/core/setup.rs | 8 ++++- src/main.rs | 1 + src/services/certs/acme.rs | 10 +++++- 10 files changed, 83 insertions(+), 27 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 750ac8a..d1afa00 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -6,3 +6,6 @@ reorder_modules = true match_block_trailing_comma = true use_field_init_shorthand = true use_small_heuristics = "Off" +# clean up multiple use crate:: that share the same root import +# run with cargo +nightly fmt +imports_granularity = "Crate" diff --git a/Cargo.lock b/Cargo.lock index ef5bd9b..5cd9b62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2160,6 +2160,7 @@ dependencies = [ "reqwest", "rustls", "serde", + "serde_json", "thiserror 2.0.20", "tokio", "toml", @@ -2667,9 +2668,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 03ab67c..2a8f7d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ toml = "1.1.4" # Json serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" # Others thiserror = "2.0.20" diff --git a/src/config.rs b/src/config.rs index 9f33640..3669513 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,12 +15,13 @@ const CONFIG_PATH_ENV: &str = "CONFIG_PATH"; const CERT_DIR_ENV: &str = "CERT_DIR"; const HTTP_PORT_ENV: &str = "HTTP_PORT"; const HTTPS_PORT_ENV: &str = "HTTPS_PORT"; +pub const ACME_CHALLENGE_PREFIX: &str = "_acme-challenge."; const CERT_DIR_DEFAULT: &str = ".certs/"; // this will be stored in the .certs/ or depending on where the user wants to store it const CERT_CREDENTIAL_FILE: &str = "acme_account"; -const HTTP_PORT_DEFAULT: u16 = 80; -const HTTPS_PORT_DEFAULT: u16 = 443; +const HTTP_PORT_DEFAULT: u16 = 880; +const HTTPS_PORT_DEFAULT: u16 = 8443; const INPUT_ADDRESS: &str = "0.0.0.0"; /// in seconds diff --git a/src/core/handlers/certs.rs b/src/core/handlers/certs.rs index 624e1d3..ec2980f 100644 --- a/src/core/handlers/certs.rs +++ b/src/core/handlers/certs.rs @@ -4,8 +4,8 @@ use crate::{ handlers::filesystem::{check_file_exists, read_file, safe_path, write_file}, models::{ certs::{ - CertDir, CertPath, CertificateConfig, CertificateType, Email, KeyPath, TlsMaterial, - TlsStore, + CertAccountPath, CertDir, CertPath, CertificateConfig, CertificateType, Email, + KeyPath, TlsMaterial, TlsStore, }, dns::{Cloudflare, CloudflareProvider, DnsProvider, ProviderCredentail}, routes::Host, @@ -15,7 +15,7 @@ use crate::{ error, info, services::{ certs::{ - acme::{create_account, create_order}, + acme::{create_account, create_order, init_account, load_account}, self_signed::create_self_signed_certificate_files, }, http::create_client, @@ -24,10 +24,10 @@ use crate::{ }; use arc_swap::ArcSwap; use async_trait::async_trait; -use instant_acme::{Identifier, NewOrder, OrderStatus}; +use instant_acme::{Account, AccountCredentials, Identifier, NewOrder, OrderStatus}; use pingora::{server::ShutdownWatch, services::background::BackgroundService, tls}; use rcgen::{CertifiedKey, generate_simple_self_signed}; -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::{collections::HashMap, fs, sync::Arc, time::Duration}; use tokio::time; pub fn create_self_signed_certs(certificate_configs: &Vec) -> Result<()> { @@ -97,6 +97,7 @@ pub fn certificate_paths(host: &Host, cert_dir: &CertDir) -> Result<(KeyPath, Ce pub struct CertBackgroundRenewal { pub certificate_configs: Vec, + pub cert_account_path: CertAccountPath, pub task_interval: TaskInterval, pub tls_store: TlsStore, pub email: Email, @@ -105,12 +106,14 @@ pub struct CertBackgroundRenewal { impl CertBackgroundRenewal { pub fn new( certificate_configs: Vec, + cert_account_path: CertAccountPath, task_interval: TaskInterval, tls_store: TlsStore, email: Email, ) -> Self { Self { certificate_configs, + cert_account_path, task_interval, tls_store, email, @@ -132,10 +135,13 @@ impl BackgroundService for CertBackgroundRenewal { }, }; - // TODO what to do when creating an account fails (acme endpoints is 500 etc..) - let account_result = create_account(&self.email) - .await - .map_err(|e| Error::Certificate(format!("Failed with create_account: {}", e))); + let account = match resolve_acme_account(&self.cert_account_path, &self.email).await { + Ok(account) => account, + Err(err) => { + error!("Failed to resolve ACME account: {err:?}"); + return; + }, + }; let configs = self .certificate_configs @@ -144,14 +150,6 @@ impl BackgroundService for CertBackgroundRenewal { .filter(|c| c.cert_type == CertificateType::Acme); loop { - let (account, credentials) = match account_result { - Ok(ref pair) => pair, - Err(ref err) => { - error!("{err:?}"); - continue; - }, - }; - for config in configs.clone() { let order_result = create_order(&account, &config.host).await; @@ -165,7 +163,7 @@ impl BackgroundService for CertBackgroundRenewal { }, }; - let dns_service = get_dns_services(dns_service_config, "_acme-challenge."); + let dns_service = get_dns_services(dns_service_config); let mut order = match order_result { Ok(order) => order, @@ -250,12 +248,39 @@ impl BackgroundService for CertBackgroundRenewal { } } -fn get_dns_services(config: ProviderCredentail, challenge_prefix: &str) -> impl DnsProvider { +fn get_dns_services(config: ProviderCredentail) -> impl DnsProvider { match config { ProviderCredentail::Cloudflare(config) => CloudflareProvider { zone_id: config.zone_id, api_token: config.api_token, - challenge_prefix: challenge_prefix.to_string(), + challenge_prefix: config.challenge_prefix, }, } } + +async fn resolve_acme_account( + cert_account_path: &CertAccountPath, + email: &Email, +) -> Result { + // set crypto lib to load/create the account + init_account(); + + if let Ok(credentials) = get_acme_account(cert_account_path) { + return load_account(credentials).await; + } + + let (account, credentials) = create_account(email).await?; + let content = serde_json::to_vec(&credentials) + .map_err(|e| Error::Acme(format!("Failed to serialize ACME account: {}", e)))?; + write_file(cert_account_path.clone(), &content)?; + + Ok(account) +} + +// any erorr one this part we will simply create a new account +fn get_acme_account(cert_account_path: &CertAccountPath) -> Result { + let raw_content = read_file(cert_account_path)?; + + serde_json::from_slice(&raw_content) + .map_err(|e| Error::Acme(format!("Failed to parse ACME account: {}", e))) +} diff --git a/src/core/models/certs.rs b/src/core/models/certs.rs index a498a1a..cfbe9e7 100644 --- a/src/core/models/certs.rs +++ b/src/core/models/certs.rs @@ -45,4 +45,4 @@ string_newtype!(Email, derive(Deserialize)); string_newtype!(CertDir, derive(Deserialize)); // --- CERT_ACCOUNT_PATH --- -string_newtype!(CertAccountPath, derive(Deserialize)); +pub type CertAccountPath = SafePath; diff --git a/src/core/models/dns.rs b/src/core/models/dns.rs index 75c4ebe..143246e 100644 --- a/src/core/models/dns.rs +++ b/src/core/models/dns.rs @@ -1,3 +1,4 @@ +use crate::config::ACME_CHALLENGE_PREFIX; use crate::core::models::routes::Host; use crate::string_newtype; use crate::{Error, Result}; @@ -14,6 +15,10 @@ pub trait DnsProvider: Send + Sync + 'static { fn update_challenge_record(&self, host: &Host, value: &str) -> Result; } +fn default_challenge_prefix() -> String { + ACME_CHALLENGE_PREFIX.to_string() +} + // --- DNS Record --- string_newtype!(Record, derive(Deserialize)); @@ -34,6 +39,11 @@ pub enum ProviderCredentail { pub struct CloudflareProvider { pub zone_id: ZoneId, pub api_token: ApiToken, + // challenge_prefix is a default value from ACME, + // this can not be set or changed from the userside + // but its here to keep keep it as a single config + // per provider + #[serde(skip, default = "default_challenge_prefix")] pub challenge_prefix: String, } diff --git a/src/core/setup.rs b/src/core/setup.rs index 2da6167..4be3f61 100644 --- a/src/core/setup.rs +++ b/src/core/setup.rs @@ -9,7 +9,9 @@ use crate::{ proxy::run_proxy, }, models::{ - certs::{CertDir, CertificateConfig, CertificateType, Email, TlsStore}, + certs::{ + CertAccountPath, CertDir, CertificateConfig, CertificateType, Email, TlsStore, + }, proxy::{ProxyConfig, ProxyRoute, ProxyTls}, routes::Route, tasks::TaskInterval, @@ -48,6 +50,7 @@ impl HandleFileSystem { pub struct HandleCertificates { certificate_configs: Vec, + cert_account_path: CertAccountPath, task_interval: TaskInterval, email: Email, } @@ -55,6 +58,7 @@ pub struct HandleCertificates { impl HandleCertificates { pub fn new( cert_dir: CertDir, + cert_account_path: CertAccountPath, email: Email, routes: Vec, task_interval: TaskInterval, @@ -71,6 +75,7 @@ impl HandleCertificates { Self { certificate_configs, + cert_account_path, task_interval, email, } @@ -82,6 +87,7 @@ impl HandleCertificates { let store = load_tls_store(&self.certificate_configs)?; let renewal = CertBackgroundRenewal::new( self.certificate_configs.clone(), + self.cert_account_path.clone(), self.task_interval.clone(), store.clone(), self.email.clone(), diff --git a/src/main.rs b/src/main.rs index bf0e8fd..1a54adc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ fn main() -> Result<()> { // and spin up background tasks to refresh certificates let cert_handler = HandleCertificates::new( config.cert_dir.clone(), + config.cert_account_path.clone(), config.email, config.routes.clone(), config.task_interval, diff --git a/src/services/certs/acme.rs b/src/services/certs/acme.rs index 99c7c05..4aadaf4 100644 --- a/src/services/certs/acme.rs +++ b/src/services/certs/acme.rs @@ -4,13 +4,17 @@ use crate::{ certs::{CertificateConfig, Email}, routes::Host, }, + info, }; use instant_acme::{ Account, AccountCredentials, Identifier, LetsEncrypt, NewAccount, NewOrder, Order, }; use rustls::crypto::CryptoProvider; -// w +pub fn init_account() { + CryptoProvider::install_default(rustls::crypto::ring::default_provider()); +} + pub async fn load_account(credentials: AccountCredentials) -> Result { let account = Account::builder() .map_err(|e| Error::Acme(e.to_string()))? @@ -18,6 +22,8 @@ pub async fn load_account(credentials: AccountCredentials) -> Result { .await .map_err(|e| Error::Acme(e.to_string()))?; + info!("Reusing ACME account"); + Ok(account) } @@ -41,6 +47,8 @@ pub async fn create_account(email: &Email) -> Result<(Account, AccountCredential .await .map_err(|e| Error::Acme(e.to_string()))?; + info!("Created new ACME account"); + Ok((account, credentials)) } From db7d99d28ed249fc441f61fdf492a3b3366675f0 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:26:29 +0200 Subject: [PATCH 14/15] merge imports --- src/core/models/certs.rs | 3 +-- src/core/models/dns.rs | 7 +++---- src/services/dns/cloudflare.rs | 10 +++++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/core/models/certs.rs b/src/core/models/certs.rs index cfbe9e7..4ad5470 100644 --- a/src/core/models/certs.rs +++ b/src/core/models/certs.rs @@ -8,8 +8,7 @@ use pingora::tls::{ x509::X509, }; use serde::Deserialize; -use std::collections::HashMap; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; #[derive(Debug, Clone, Deserialize, Default, PartialEq)] #[serde(rename_all = "snake_case")] diff --git a/src/core/models/dns.rs b/src/core/models/dns.rs index 143246e..e16a458 100644 --- a/src/core/models/dns.rs +++ b/src/core/models/dns.rs @@ -1,7 +1,6 @@ -use crate::config::ACME_CHALLENGE_PREFIX; -use crate::core::models::routes::Host; -use crate::string_newtype; -use crate::{Error, Result}; +use crate::{ + Error, Result, config::ACME_CHALLENGE_PREFIX, core::models::routes::Host, string_newtype, +}; use reqwest::Client; use serde::Deserialize; diff --git a/src/services/dns/cloudflare.rs b/src/services/dns/cloudflare.rs index 7bb4ee8..b41363a 100644 --- a/src/services/dns/cloudflare.rs +++ b/src/services/dns/cloudflare.rs @@ -1,8 +1,12 @@ use serde::de::value; -use crate::core::models::dns::{CloudflareProvider, DnsProvider, Record}; -use crate::core::models::routes::Host; -use crate::{Error, Result}; +use crate::{ + Error, Result, + core::models::{ + dns::{CloudflareProvider, DnsProvider, Record}, + routes::Host, + }, +}; impl DnsProvider for CloudflareProvider { fn get_challenge_record(&self, host: &Host) -> Result { From 4ae280172f382019807bb4df66f99453d822d892 Mon Sep 17 00:00:00 2001 From: Max van der Schee <26795741+mvdschee@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:51:27 +0200 Subject: [PATCH 15/15] clean up readme --- README.md | 187 +++++++++++++++++++++---------------------- docker-compose.yml | 4 +- example/example.toml | 15 ++++ 3 files changed, 109 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index d4d63d7..705aa30 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,34 @@ -# Reverse Proxy +# reverse-proxy -> A small, fast, single-binary reverse proxy with TLS termination and per-host routing. Drop in a TOML file, point DNS at it, done. +A reverse proxy for self-hosting, in a single binary. You point it at your hosts in a TOML file, it terminates TLS where you ask it to, and forwards to your upstreams. Built on Cloudflare's [Pingora](https://github.com/cloudflare/pingora) (the engine behind a chunk of their edge) with BoringSSL for TLS, and shipped as a hardened container image: non-root, read-only rootfs, no shell. -

Architecture diagram

- -Built in Rust on top of Cloudflare's [Pingora](https://github.com/cloudflare/pingora), shipped as a hardened container (~few MB, non-root, read-only rootfs). Designed for self-hosting: one process in front of all your services, configured from a single file. +

A request flowing through the proxy

## Why -I normally use `nginxproxy/nginx-proxy` for my self-hosted projects. It works, but every container has to live on the same Docker network with a `VIRTUAL_HOST` label. That couples the proxy to the apps in a way I don't love. +I run `nginxproxy/nginx-proxy` for my self-hosted projects. It works, but it only talks to apps that live on the same Docker network with `VIRTUAL_HOST` labels, and that coupling between the proxy and my deployment setup never sat right with me. -So I wrote my own (by hand, old-school, no AI writing the code) to learn the internals and end up with something that: +So I wrote my own (by hand, old-school, no AI writing the code) to learn how a proxy actually works. What came out of it: -- Has **one place** to declare what gets proxied (a TOML file). -- Doesn't care how upstreams are deployed: containers, host processes, anywhere reachable. -- Boots fast, binds two ports, terminates TLS, forwards. That's it. +- One file declares what gets proxied. +- It doesn't care how your apps are deployed. Container, host process, a machine on the LAN, anything it can reach. +- It boots, binds 80 and 443, terminates TLS, forwards. That's it. ## Features -| Status | Feature | -| ------ | -------------------------------------------------------- | -| done | HTTP & HTTPS listeners (Pingora + BoringSSL) | -| done | Per-host routing from a single `config.toml` | -| done | Self-signed certificate generation (`rcgen`) on boot | -| done | Per-route TLS mode: `self_signed`, `acme`, `none` | -| done | HTTP → HTTPS redirect (301) for routes with a cert | -| done | Hardened container: non-root, read-only fs, dropped caps | -| done | Reach apps on the host via `host.docker.internal` | -| wip | Let's Encrypt / ACME issuance (`rustls-acme`) | -| todo | Structured logs (errors today, access logs next) | -| todo | Full test coverage for behavioural guarantees | -| todo | Performance benchmarks | +- HTTP and HTTPS listeners (both always on) +- Routing by `Host` header from a single TOML file +- Self-signed certificates, generated on boot for `self_signed` routes +- A 301 redirect from HTTP to HTTPS for routes that have a cert +- ACME / Let's Encrypt: account creation, an hourly renewal loop, and a Cloudflare DNS provider (end-to-end issuance is still WIP, see the roadmap) +- A hardened container image: non-root, read-only rootfs, all capabilities dropped +- Multi-arch images (amd64, arm64) published to Docker Hub and GHCR per release ## Quick start -The image is published to Docker Hub as `maxvanderschee/reverse-proxy` (also available on GHCR as `ghcr.io/mvdschee/reverse-proxy`). Two things to provide: +You provide two things: a `config.toml` with your hosts, and a compose file. -1. A `config.toml` describing your routes. -2. A `docker-compose.yml` that mounts it. +The image is published as `maxvanderschee/reverse-proxy` on Docker Hub and as `ghcr.io/mvdschee/reverse-proxy` on GHCR. You get multi-arch builds (amd64, arm64) per release tag, plus `latest`. **`config.toml`** @@ -54,6 +45,23 @@ cert_type = "none" host = "api.example.com" upstream = "host.docker.internal:8000" cert_type = "self_signed" + +# ACME is still a work in progress (see the roadmap): the account, the +# renewal loop and the DNS provider are in, but an order is never finalized, +# so until that lands this route gets no cert and its HTTPS side won't serve +# TLS. Keep it here as a shape reference, or flip it to self_signed if you +# want this host actually working. +[[routes]] +host = "blog.example.com" +upstream = "host.docker.internal:2000" +cert_type = "acme" + +# needed for the DNS-01 challenge. the renewal loop skips acme routes that +# don't have a provider. the challenge prefix is always _acme-challenge., +# it can't be configured. +[routes.dns_provider.cloudflare] +zone_id = "your-cloudflare-zone-id" +api_token = "your-cloudflare-api-token" ``` **`docker-compose.yml`** @@ -63,8 +71,8 @@ services: proxy: image: maxvanderschee/reverse-proxy:latest # or ghcr.io/mvdschee/reverse-proxy:latest restart: unless-stopped - # host ports 80/443 mapped to container ports 8080/8443 - # see ## Under the hood for explanation + # the image runs non-root and can't bind 80/443, hence 8080/8443 + # (see "The container image" below) ports: - "80:8080" - "443:8443" @@ -78,100 +86,91 @@ services: - no-new-privileges:true cap_drop: - ALL - tmpfs: - - /tmp + healthcheck: + test: ["CMD", "nc", "-z", "127.0.0.1", "8080"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s volumes: proxy-certs: ``` -Then: - -```sh -docker compose up -d -``` - -Point DNS at the host, and traffic on `:80`/`:443` for the configured hosts will be terminated and proxied to the upstreams. +Then `docker compose up -d`, point your DNS at the box, and you're done. Requests for a configured host get proxied, and everything else gets a `421 Misdirected Request`. -## Configuration +## Config -The full schema lives in [`example/example.toml`](example/example.toml). The fields: +The file is small: an `[acme]` table and a list of `[[routes]]`. The full schema, with comments, lives in [`example/example.toml`](example/example.toml). -| Field | Required | Description | -| -------------------- | -------- | ----------------------------------------------------------------------------------------------------- | -| `acme.email` | yes | Contact email for Let's Encrypt (used once ACME lands; required today even if every route is `none`). | -| `routes[].host` | yes | The `Host` header to match (e.g. `app.example.com`). | -| `routes[].upstream` | yes | `host:port` to forward to. Use `host.docker.internal:` to reach the host machine from Docker. | -| `routes[].cert_type` | no | `self_signed` (default works on boot), `acme` (WIP), or `none` (HTTP only). Defaults to `none`. | +| Field | Required | What it does | +| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `acme.email` | yes | Contact email for the Let's Encrypt account. Required by the parser even if every route is `none`. | +| `routes[].host` | yes | The `Host` header to match, e.g. `app.example.com`. | +| `routes[].upstream` | yes | The `host:port` to forward to, over plain HTTP. Use `host.docker.internal:` to reach apps running on the Docker host. | +| `routes[].cert_type` | no | `none` (HTTP only), `self_signed`, or `acme` (WIP, see the roadmap). Defaults to `none`. | +| `routes[].dns_provider` | no | Only meaningful for `acme` routes. The DNS provider used for the DNS-01 challenge. Currently Cloudflare only (`zone_id` + `api_token`); `acme` routes without it are skipped by the renewal loop. | -## How it works +A couple of things worth knowing: -``` - ┌──────────────────────────────────────────┐ - │ Pingora process │ -client │ ┌────────────────┐ ┌──────────────┐ │ HTTP - ───► │ │ TLS termination│ ──►│ Host router │ ─┼──────► upstream - │ └────────────────┘ └──────────────┘ │ - │ (BoringSSL) (config.toml) │ - └──────────────────────────────────────────┘ -``` - -On startup the binary: +- `host` is matched exactly against the `Host` header. A request for `app.example.com:443` will not match a route for `app.example.com`. That's a deliberate strictness, not a bug. +- A route counts as "TLS" when its `cert_type` is anything other than `none`. TLS routes get the 301 on the HTTP listener, and the HTTPS listener serves them with the cert in `CERT_DIR` (`.pem` / `.key`). `self_signed` routes get one on boot, `acme` routes get one once the issuance flow finishes. +- If a route's cert files are missing at startup, the proxy boots anyway and logs a warning. -1. Reads `config.toml` from `CONFIG_PATH`. -2. Ensures `CERT_DIR` exists and is writable. -3. Generates self-signed certs via `rcgen` for any route configured as `self_signed`. -4. Boots Pingora with one HTTPS listener (SNI-routed) and one HTTP listener, both reading the same per-host route table. The HTTP listener 301-redirects to HTTPS for any route that has a cert. +## Environment variables -Routing is purely `Host`-based: incoming `Host` header → route entry → forward to upstream over plain HTTP. +| Variable | Binary default | In the image | What it does | +| ------------- | -------------- | ------------------------ | -------------------------------------------------------------------------------- | +| `CONFIG_PATH` | _none, exits_ | `/etc/proxy/config.toml` | Path to the TOML config. The binary exits without one. | +| `CERT_DIR` | `.certs/` | `/var/lib/proxy/certs` | Where certs are written, and where the ACME account file (`acme_account`) lives. | +| `HTTP_PORT` | `80` | `8080` | Port for the HTTP listener. | +| `HTTPS_PORT` | `443` | `8443` | Port for the HTTPS listener. | -## Under the hood +The image bakes in the right-hand column (the Dockerfile sets all four), which is why the compose file above doesn't set any of them: it just mounts your config at the path the image already expects, and maps `80`/`443` onto `8080`/`8443`. The port remap exists because the container runs as `nonroot` (uid 65532) with all capabilities dropped, and without `CAP_NET_BIND_SERVICE` a process can't bind a port below 1024. -A few decisions worth knowing about if you want to dig in or contribute. +The listeners always bind `0.0.0.0`. -**Pingora + BoringSSL.** The proxy is built on Pingora (the engine behind a chunk of Cloudflare's edge), with the `boringssl` feature instead of the OpenSSL default. That keeps the binary self-contained and avoids dragging system OpenSSL in. +## The container image -**Why `8080`/`8443` inside the container.** The image runs as user `nonroot` (uid `65532`) with `cap_drop: ALL` and `no-new-privileges`. A process without `CAP_NET_BIND_SERVICE` can't bind to ports below 1024, so the binary listens on `8080`/`8443` and the compose file maps the standard ports onto them. The defaults of the binary itself (`HTTP_PORT`/`HTTPS_PORT` env vars) are `80`/`443`; the Dockerfile overrides them. +Both build and runtime stages use the [Docker Hardened Images](https://hub.docker.com/hardened-images/catalog) alpine base. The runtime image ships the binary, `libgcc_s.so.1` (the binary is dynamically linked and needs the unwinder at runtime), musl, busybox, and CA certificates. There's no shell and no package manager in there. -**Hardened base image.** Built and runtime images are both [Docker Hardened Images](https://hub.docker.com/hardened-images/catalog) (alpine-base). Runtime image ships only the binary, `musl`, `ca-certs`, and `libgcc_s.so.1` (needed by the dynamically-linked binary's unwinder). No shell, no package manager, no extras. +One build detail that is a little unusual: we cross-compile to `*-unknown-linux-musl` but with `-crt-static` disabled. Fully static musl binaries can't `dlopen`, which breaks bindgen's libclang loader during the build. The runtime image ships musl, so the resulting (mostly dynamic) binary runs fine on it. -**Static-ish musl build.** Cross-compiled to `*-unknown-linux-musl`, but with `-crt-static` disabled so that build-script artifacts (notably `bindgen`'s `dlopen` of `libclang`) work. The runtime image ships `musl`, so the binary still runs cleanly. +The proxy runs on the public internet, so I harden the container: -**No async runtime juggling.** `main()` is synchronous. Pingora owns its own Tokio runtime, so the binary just initializes config, certs, and hands control to Pingora's server loop. +- `read_only: true` — the proxy only writes its certs, and those go to the named volume. There's no reason the rest of the filesystem should be writable, so if it gets compromised it can't alter its own binary or config. +- `cap_drop: ALL` and `no-new-privileges: true` — run with no capabilities and no setuid escalation. If the proxy is compromised, the damage stays in the process. -**Per-route TLS modes.** Each route picks its own cert strategy. `none` skips TLS entirely (handy for an internal-only host). `self_signed` writes a fresh cert on every restart. `rcgen`'s defaults give it a ~2000-year validity, so there's no in-process renewal loop; the restart is the rotation. `acme` is wired into the type system but not yet issuing. +## Internals -## Environment variables +A few implementation notes if you want to dig in or contribute: -| Var | Default | Purpose | -| ------------- | ------------------------- | ------------------------------------------- | -| `CONFIG_PATH` | _required_ | Path to `config.toml`. | -| `CERT_DIR` | `.certs/` | Where generated/issued certs are persisted. | -| `HTTP_PORT` | `80` (image sets `8080`) | Port the HTTP listener binds to. | -| `HTTPS_PORT` | `443` (image sets `8443`) | Port the HTTPS listener binds to. | +- `main()` is synchronous. Pingora owns its own Tokio runtime, so the binary just loads the config, sorts out the certs, and hands over to the server loop. +- The cert store is a `HashMap` of host to (cert, key) behind an `ArcSwap`. When the renewal loop is finished it can swap in a new cert without a restart. The swap isn't implemented yet; that's the missing piece of the ACME work. +- The ACME account credentials are persisted in `CERT_DIR/acme_account`, so a restart reuses the account instead of registering a new one every time. +- A background task wakes up every hour and drives the renewal loop for any `acme` routes. ## Roadmap -In rough priority order: +Roughly in the order I'm doing things: + +1. **Finish the ACME flow.** Create and verify the DNS challenge records, confirm the record is in place before finalizing, issue a staging cert first, then prod, and swap the renewed cert into the store. Until this is done, `acme` routes won't get real certs. +2. **Access logs and a proper logging story.** Errors go to stdout today. +3. **A test suite for the behavioural bits.** Routing, redirects, cert handling, error paths. +4. **Benchmarks.** Baseline numbers to compare against nginx, so I can judge changes by measurement instead of vibes. +5. **Idiomatic Rust.** V1 was correctness-first. The internals get tidied up once the shape is stable. -1. **ACME issuance.** Wire up `rustls-acme` so the `acme` cert_type flips from "wired" to "working". -2. **Structured logging.** Error logs are in place; access logs and a sane structured format come next. -3. **Full test coverage.** Behavioural guarantees (routing, TLS modes, redirect, error paths) backed by tests, not just manual checks. -4. **Performance benchmarks.** Measure baseline throughput/latency vs. nginx, so future changes can be judged against numbers instead of vibes. -5. **Make each module idiomatic.** V1 is correctness-first; polish the internals once the surface is stable. +## On AI assistance -## AI disclaimer +I'm upfront about how much AI I use on my projects. This one the Rust is written by hand, I wanted to understand every line of it, and this was mostly an excuse to learn how a proxy works. Where I did use AI: -I'll be upfront on every public project about what was done with AI. For this one, I wanted to write the Rust myself to keep the skills sharp and have 100% understanding of every bit of code. AI was used for: +- Researching trade-offs (Pingora vs. the alternatives, BoringSSL vs. Rustls, musl vs. glibc) +- Drafting and polishing this README +- Dockerfile, I have done enough of those. Really did not feel like writing that by hand :) -- Research and tradeoff discussions -- Cleanup of the README and other prose -- Discussing code-level solutions -- Generating the Docker image scaffolding from a spec +The code itself is mine, so if you find a bug, that's on me. -# TODO: +## Contributing -- finish create_acme_dns_challenge -- create a dns entry checker for the background task -- generate a staging certificate with instance_acme -- wire up the full flow in CertBackgroundRenewal +- **Open an issue first.** I'd rather talk through what you want to change, and how, in an issue than review a random PR. +- **No AI-written code.** The Rust in this repo is written by hand, and I'd like it to stay that way. It's fine to use AI to help you write a change, but you must understand the code you're changing. This is a Rust project, and your profile should show it. diff --git a/docker-compose.yml b/docker-compose.yml index a289748..469c2b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: proxy: # Replace with the actual image - # image: ghcr.io/mvdschee/proxy:latest + # image: ghcr.io/mvdschee/reverse-proxy:latest build: context: . dockerfile: .docker/Dockerfile @@ -24,8 +24,6 @@ services: - no-new-privileges:true cap_drop: - ALL - tmpfs: - - /tmp healthcheck: # Busybox `nc -z` confirms the HTTP listener is bound. It does # not exercise the proxy path; the listener simply has to accept. diff --git a/example/example.toml b/example/example.toml index 6eb0a62..c8a6494 100644 --- a/example/example.toml +++ b/example/example.toml @@ -10,3 +10,18 @@ cert_type = "self_signed" # default is "none" host = "test.example.com" upstream = "127.0.0.1:3000" # use "host.docker.internal:3000" when using docker cert_type = "none" # default is "none" + +# acme needs a dns provider for the DNS-01 challenge. the background renewal +# loop skips acme routes that have none. cert_type = "acme" is still WIP: +# the order is created and the dns provider is in, but the challenge is never +# finalized, so no cert is issued yet. +[[routes]] +host = "blog.example.com" +upstream = "127.0.0.1:3000" # use "host.docker.internal:3000" when using docker +cert_type = "acme" + +# which dns provider to use for the DNS-01 challenge. cloudflare is the only +# one for now. the challenge prefix is always _acme-challenge. and fixed. +[routes.dns_provider.cloudflare] +zone_id = "your-cloudflare-zone-id" +api_token = "your-cloudflare-api-token"