From f1257d2285b8182fb6498f5d59b795d9fd4305ca Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 16:33:49 +0200 Subject: [PATCH 01/36] packages/ak-arbiter: init Signed-off-by: Marc 'risson' Schmitt --- .cargo/config.toml | 2 + .dockerignore | 2 +- CODEOWNERS | 1 + Cargo.lock | 72 ++++++++ Cargo.toml | 25 ++- packages/ak-arbiter/Cargo.toml | 20 +++ packages/ak-arbiter/src/lib.rs | 291 +++++++++++++++++++++++++++++++++ 7 files changed, 408 insertions(+), 5 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 packages/ak-arbiter/Cargo.toml create mode 100644 packages/ak-arbiter/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000000..bff29e6e175b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/.dockerignore b/.dockerignore index 75c223bfdcfa..f8e411e9bb21 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,4 +10,4 @@ build_docs/** blueprints/local .git .venv -target/ +target diff --git a/CODEOWNERS b/CODEOWNERS index 74346050246c..01971905f207 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -27,6 +27,7 @@ Makefile @goauthentik/infrastructure .editorconfig @goauthentik/infrastructure CODEOWNERS @goauthentik/infrastructure # Backend packages +packages/ak-* @goauthentik/backend packages/client-rust @goauthentik/backend packages/django-channels-postgres @goauthentik/backend packages/django-postgres-cache @goauthentik/backend diff --git a/Cargo.lock b/Cargo.lock index b6b1d8d7583e..02247cf37344 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -84,6 +93,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "authentik-arbiter" +version = "0.0.0" +dependencies = [ + "axum-server", + "eyre", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "authentik-client" version = "2026.5.0-rc1" @@ -146,6 +166,28 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" @@ -461,6 +503,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -658,6 +710,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -672,6 +730,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1814,6 +1873,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -1907,9 +1967,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" diff --git a/Cargo.toml b/Cargo.toml index b47bfb0b50ea..beb2d205e997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,9 @@ [workspace] -members = ["packages/client-rust", "website/scripts/docsmg"] +members = [ + "packages/ak-arbiter", + "packages/client-rust", + "website/scripts/docsmg", +] resolver = "3" [workspace.package] @@ -14,6 +18,7 @@ license-file = "LICENSE" publish = false [workspace.dependencies] +axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] } aws-lc-rs = { version = "= 1.16.2", features = ["fips"] } clap = { version = "= 4.6.0", features = ["derive", "env"] } colored = "= 3.1.1" @@ -42,11 +47,16 @@ serde_repr = "= 0.1.20" serde_with = { version = "= 3.18.0", default-features = false, features = [ "base64", ] } -tokio = { version = "= 1.50.0", features = ["full"] } +tokio = { version = "= 1.50.0", features = ["full", "tracing"] } tokio-util = { version = "= 0.7.18", features = ["full"] } +tracing = "= 0.1.44" url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } +authentik-arbiter = { path = "./packages/ak-arbiter", package = "arbiter" } + +authentik-client = { path = "./packages/client-rust" } + [profile.dev.package.backtrace] opt-level = 3 @@ -89,12 +99,20 @@ perf = { priority = -1, level = "warn" } style = { priority = -1, level = "warn" } suspicious = { priority = -1, level = "warn" } ### and disable the ones we don't want +### cargo group +multiple_crate_versions = "allow" ### pedantic group +missing_errors_doc = "allow" +missing_panics_doc = "allow" +must_use_candidate = "allow" redundant_closure_for_method_calls = "allow" +struct_field_names = "allow" too_many_lines = "allow" ### nursery -redundant_pub_crate = "allow" +missing_const_for_fn = "allow" option_if_let_else = "allow" +redundant_pub_crate = "allow" +significant_drop_tightening = "allow" ### restriction group allow_attributes = "warn" allow_attributes_without_reason = "warn" @@ -107,7 +125,6 @@ create_dir = "warn" dbg_macro = "warn" default_numeric_fallback = "warn" disallowed_script_idents = "warn" -doc_paragraphs_missing_punctuation = "warn" empty_drop = "warn" empty_enum_variants_with_brackets = "warn" empty_structs_with_brackets = "warn" diff --git a/packages/ak-arbiter/Cargo.toml b/packages/ak-arbiter/Cargo.toml new file mode 100644 index 000000000000..2435194a3703 --- /dev/null +++ b/packages/ak-arbiter/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "authentik-arbiter" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +publish.workspace = true + +[dependencies] +axum-server.workspace = true +eyre.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tracing.workspace = true + +[lints] +workspace = true diff --git a/packages/ak-arbiter/src/lib.rs b/packages/ak-arbiter/src/lib.rs new file mode 100644 index 000000000000..b5a4af851d71 --- /dev/null +++ b/packages/ak-arbiter/src/lib.rs @@ -0,0 +1,291 @@ +//! Utilities to manage long running tasks, such as servers and watchers, and events propagated +//! between those tasks. +//! +//! Also manages signals sent to the main process. + +use std::{net, os::unix, sync::Arc, time::Duration}; + +use axum_server::Handle; +use eyre::{Report, Result}; +use tokio::{ + signal::unix::{Signal, SignalKind, signal}, + sync::{Mutex, broadcast}, + task::{JoinSet, join_set::Builder}, +}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFuture}; +use tracing::info; + +/// All the signal streams we watch for. We don't create those directly in [`watch_signals`] +/// because that would prevent us from handling errors early. +struct SignalStreams { + hup: Signal, + int: Signal, + quit: Signal, + usr1: Signal, + usr2: Signal, + term: Signal, +} + +impl SignalStreams { + fn new() -> Result { + Ok(Self { + hup: signal(SignalKind::hangup())?, + int: signal(SignalKind::interrupt())?, + quit: signal(SignalKind::quit())?, + usr1: signal(SignalKind::user_defined1())?, + usr2: signal(SignalKind::user_defined2())?, + term: signal(SignalKind::terminate())?, + }) + } +} + +/// Watch for incoming signals and either shutdown the application or dispatch them to receivers. +async fn watch_signals(streams: SignalStreams, arbiter: Arbiter) -> Result<()> { + info!("starting signals watcher"); + let SignalStreams { + mut hup, + mut int, + mut quit, + mut usr1, + mut usr2, + mut term, + } = streams; + loop { + tokio::select! { + _ = hup.recv() => { + info!("signal HUP received"); + arbiter.do_fast_shutdown().await; + }, + _ = int.recv() => { + info!("signal INT received"); + arbiter.do_fast_shutdown().await; + }, + _ = quit.recv() => { + info!("signal QUIT received"); + arbiter.do_fast_shutdown().await; + }, + _ = usr1.recv() => { + info!("signal URS1 received"); + arbiter.send_event(SignalKind::user_defined1().into())?; + }, + _ = usr2.recv() => { + info!("USR2 received."); + arbiter.send_event(SignalKind::user_defined2().into())?; + }, + _ = term.recv() => { + info!("signal TERM received"); + arbiter.do_graceful_shutdown().await; + }, + () = arbiter.shutdown() => { + info!("stopping signals watcher"); + return Ok(()); + } + }; + } +} + +/// Manager for long running tasks, such as servers and watchers. +pub struct Tasks { + tasks: JoinSet>, + arbiter: Arbiter, +} + +impl Tasks { + /// Create a new [`Tasks`] manager. + /// + /// # Errors + /// + /// Errors if the creation of signals watcher fails. + pub fn new() -> Result { + let mut tasks = JoinSet::new(); + let arbiter = Arbiter::new(&mut tasks)?; + + Ok(Self { tasks, arbiter }) + } + + /// Build a new task. See [`tokio::task::JoinSet::build_task`] for details. + pub fn build_task(&mut self) -> Builder<'_, Result<()>> { + self.tasks.build_task() + } + + /// Get an [`Arbiter`]. + pub fn arbiter(&self) -> Arbiter { + self.arbiter.clone() + } + + /// Run the tasks until completion. If one of them fails, terminate the program immediately. + pub async fn run(self) -> Vec { + let Self { mut tasks, arbiter } = self; + + let mut errors = Vec::new(); + + if let Some(result) = tasks.join_next().await { + arbiter.do_graceful_shutdown().await; + + match result { + Ok(Ok(())) => {} + Ok(Err(err)) => { + arbiter.do_fast_shutdown().await; + errors.push(err); + } + Err(err) => { + arbiter.do_fast_shutdown().await; + errors.push(Report::new(err)); + } + } + + while let Some(result) = tasks.join_next().await { + match result { + Ok(Ok(())) => {} + Ok(Err(err)) => errors.push(err), + Err(err) => errors.push(Report::new(err)), + } + } + } + + errors + } +} + +/// Manage shutdown state and several communication channels. +#[derive(Clone)] +pub struct Arbiter { + /// Token to shutdown the application immediately. + fast_shutdown: CancellationToken, + /// Token to shutdown the application gracefully. + graceful_shutdown: CancellationToken, + /// Token set when any shutdown is triggered. + shutdown: CancellationToken, + + /// axum-server [`Handle`] to manage. + net_handles: Arc>>>, + unix_handles: Arc>>>, + + /// Broadcaster of program-wide events, except shutdown which is handled by tokens above. + events_tx: broadcast::Sender, +} + +impl Arbiter { + fn new(tasks: &mut JoinSet>) -> Result { + let (events_tx, _events_rx) = broadcast::channel(1024); + let arbiter = Self { + fast_shutdown: CancellationToken::new(), + graceful_shutdown: CancellationToken::new(), + shutdown: CancellationToken::new(), + + // 5 is http, https, metrics and a bit of room + net_handles: Arc::new(Mutex::new(Vec::with_capacity(5))), + // 2 is http and metrics + unix_handles: Arc::new(Mutex::new(Vec::with_capacity(2))), + + events_tx, + }; + + let streams = SignalStreams::new()?; + + tasks + .build_task() + .name(&format!("{}::watch_signals", module_path!())) + .spawn(watch_signals(streams, arbiter.clone()))?; + + Ok(arbiter) + } + + /// Add a new [`Handle`] to be managed, specifically for [`net::SocketAddr`] addresses. + /// + /// This handle will be shutdown when this arbiter is shutdown. + pub async fn add_net_handle(&self, handle: Handle) { + self.net_handles.lock().await.push(handle); + } + + /// Add a new [`Handle`] to be managed, specifically for [`unix::net::SocketAddr`] addresses. + /// + /// This handle will be shutdown when this arbiter is shutdown. + pub async fn add_unix_handle(&self, handle: Handle) { + self.unix_handles.lock().await.push(handle); + } + + /// Future that will complete when the application needs to shutdown immediately. + pub fn fast_shutdown(&self) -> WaitForCancellationFuture<'_> { + self.fast_shutdown.cancelled() + } + + /// Future that will complete when the application needs to shutdown gracefully. + pub fn graceful_shutdown(&self) -> WaitForCancellationFuture<'_> { + self.graceful_shutdown.cancelled() + } + + /// Future that will complete when the application needs to shutdown, either immediately or + /// gracefully. It's a helper so users that don't make the difference between immediate and + /// graceful shutdown don't need to handle two scenarios. + pub fn shutdown(&self) -> WaitForCancellationFuture<'_> { + self.shutdown.cancelled() + } + + /// Shutdown the application immediately. + async fn do_fast_shutdown(&self) { + info!("arbiter has been told to shutdown immediately"); + self.unix_handles + .lock() + .await + .iter() + .for_each(Handle::shutdown); + self.net_handles + .lock() + .await + .iter() + .for_each(Handle::shutdown); + info!("all webservers have been shutdown, shutting down the other tasks immediately"); + self.fast_shutdown.cancel(); + self.shutdown.cancel(); + } + + /// Shutdown the application gracefully. + async fn do_graceful_shutdown(&self) { + info!("arbiter has been told to shutdown gracefully"); + // Match the value in lifecycle/gunicorn.conf.py for graceful shutdown + let timeout = Some(Duration::from_secs(30 + 5)); + self.unix_handles + .lock() + .await + .iter() + .for_each(|handle| handle.graceful_shutdown(timeout)); + self.net_handles + .lock() + .await + .iter() + .for_each(|handle| handle.graceful_shutdown(timeout)); + info!("all webservers have been shutdown, shutting down the other tasks gracefully"); + self.graceful_shutdown.cancel(); + self.shutdown.cancel(); + } + + /// Create a new [`broadcast::Receiver`] to listen for signals sent to the main process. This + /// may not include all signals we catch, since some of those will shutdown the application. + pub fn events_subscribe(&self) -> broadcast::Receiver { + self.events_tx.subscribe() + } + + /// Send a value on the config changes watch channel. + /// + /// # Errors + /// + /// See [`broadcast::Sender::send`]. + pub fn send_event(&self, value: Event) -> Result<()> { + self.events_tx.send(value)?; + Ok(()) + } +} + +/// Events propagated throughout the program. +#[derive(Clone, Debug)] +pub enum Event { + /// A signal has been received. + Signal(SignalKind), +} + +impl From for Event { + fn from(value: SignalKind) -> Self { + Self::Signal(value) + } +} From 5294f8a8d3d9a30e5765d0050e1c60d3d68bbb8e Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 16:38:11 +0200 Subject: [PATCH 02/36] fixup Signed-off-by: Marc 'risson' Schmitt --- Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index beb2d205e997..c5c19315e88a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,10 +53,6 @@ tracing = "= 0.1.44" url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } -authentik-arbiter = { path = "./packages/ak-arbiter", package = "arbiter" } - -authentik-client = { path = "./packages/client-rust" } - [profile.dev.package.backtrace] opt-level = 3 From 57edeec1a1049e414a25607a90f7f646ddaea771 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 17:48:51 +0200 Subject: [PATCH 03/36] add tests Signed-off-by: Marc 'risson' Schmitt --- .cargo/config.toml | 3 + Cargo.lock | 13 ++ Cargo.toml | 1 + packages/ak-arbiter/Cargo.toml | 3 + packages/ak-arbiter/src/lib.rs | 208 +++++++++++++++++++++-- website/scripts/docsmg/src/hackyfixes.rs | 6 - 6 files changed, 218 insertions(+), 16 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index bff29e6e175b..29b17fd88bb1 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,5 @@ +[alias] +t = ["nextest", "run"] + [build] rustflags = ["--cfg", "tokio_unstable"] diff --git a/Cargo.lock b/Cargo.lock index 02247cf37344..cf6080a3e7c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,7 @@ version = "0.0.0" dependencies = [ "axum-server", "eyre", + "nix", "tokio", "tokio-util", "tracing", @@ -1095,6 +1096,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" diff --git a/Cargo.toml b/Cargo.toml index c5c19315e88a..beb3bdeaa231 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ clap = { version = "= 4.6.0", features = ["derive", "env"] } colored = "= 3.1.1" dotenvy = "= 0.15.7" eyre = "= 0.6.12" +nix = { version = "= 0.31.2", features = ["signal"] } regex = "= 1.12.3" reqwest = { version = "= 0.13.2", features = [ "form", diff --git a/packages/ak-arbiter/Cargo.toml b/packages/ak-arbiter/Cargo.toml index 2435194a3703..61e3593d1af3 100644 --- a/packages/ak-arbiter/Cargo.toml +++ b/packages/ak-arbiter/Cargo.toml @@ -16,5 +16,8 @@ tokio.workspace = true tokio-util.workspace = true tracing.workspace = true +[dev-dependencies] +nix.workspace = true + [lints] workspace = true diff --git a/packages/ak-arbiter/src/lib.rs b/packages/ak-arbiter/src/lib.rs index b5a4af851d71..17004c6bd40e 100644 --- a/packages/ak-arbiter/src/lib.rs +++ b/packages/ak-arbiter/src/lib.rs @@ -64,17 +64,17 @@ async fn watch_signals(streams: SignalStreams, arbiter: Arbiter) -> Result<()> { info!("signal QUIT received"); arbiter.do_fast_shutdown().await; }, + _ = term.recv() => { + info!("signal TERM received"); + arbiter.do_graceful_shutdown().await; + }, _ = usr1.recv() => { info!("signal URS1 received"); - arbiter.send_event(SignalKind::user_defined1().into())?; + let _ = arbiter.send_event(SignalKind::user_defined1().into()); }, _ = usr2.recv() => { info!("USR2 received."); - arbiter.send_event(SignalKind::user_defined2().into())?; - }, - _ = term.recv() => { - info!("signal TERM received"); - arbiter.do_graceful_shutdown().await; + let _ = arbiter.send_event(SignalKind::user_defined2().into()); }, () = arbiter.shutdown() => { info!("stopping signals watcher"); @@ -206,11 +206,23 @@ impl Arbiter { } /// Future that will complete when the application needs to shutdown immediately. + /// + /// Consumers listening on this must also listen on [`Arbiter::graceful_shutdown`], as only one + /// of those is set upon shutdown. + /// + /// It is also possible to use [`Arbiter::shutdown`] when the behaviour is the same between a + /// fast and a graceful shutdown. pub fn fast_shutdown(&self) -> WaitForCancellationFuture<'_> { self.fast_shutdown.cancelled() } /// Future that will complete when the application needs to shutdown gracefully. + /// + /// Consumers listening on this must also listen on [`Arbiter::fast_shutdown`], as only one + /// of those is set upon shutdown. + /// + /// It is also possible to use [`Arbiter::shutdown`] when the behaviour is the same between a + /// fast and a graceful shutdown. pub fn graceful_shutdown(&self) -> WaitForCancellationFuture<'_> { self.graceful_shutdown.cancelled() } @@ -271,17 +283,18 @@ impl Arbiter { /// # Errors /// /// See [`broadcast::Sender::send`]. - pub fn send_event(&self, value: Event) -> Result<()> { - self.events_tx.send(value)?; - Ok(()) + pub fn send_event(&self, value: Event) -> Result> { + self.events_tx.send(value) } } /// Events propagated throughout the program. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { /// A signal has been received. Signal(SignalKind), + #[cfg(test)] + Noop, } impl From for Event { @@ -289,3 +302,178 @@ impl From for Event { Self::Signal(value) } } + +#[cfg(test)] +mod tests { + mod events { + use super::super::*; + use nix::sys::signal::{Signal, raise}; + + async fn signal_self(signal: Signal) { + raise(signal).expect("failed to send signal"); + tokio::time::sleep(Duration::from_millis(50)).await; + } + + #[tokio::test] + async fn signals_hup() { + let tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + + signal_self(Signal::SIGHUP).await; + + assert!(arbiter.fast_shutdown.is_cancelled()); + assert!(!arbiter.graceful_shutdown.is_cancelled()); + assert!(arbiter.shutdown.is_cancelled()); + assert_eq!(tasks.run().await.len(), 0); + } + + #[tokio::test] + async fn signals_quit() { + let tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + + signal_self(Signal::SIGQUIT).await; + + assert!(arbiter.fast_shutdown.is_cancelled()); + assert!(!arbiter.graceful_shutdown.is_cancelled()); + assert!(arbiter.shutdown.is_cancelled()); + assert_eq!(tasks.run().await.len(), 0); + } + + #[tokio::test] + async fn signals_int() { + let tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + + signal_self(Signal::SIGINT).await; + + assert!(arbiter.fast_shutdown.is_cancelled()); + assert!(!arbiter.graceful_shutdown.is_cancelled()); + assert!(arbiter.shutdown.is_cancelled()); + assert_eq!(tasks.run().await.len(), 0); + } + + #[tokio::test] + async fn signals_term() { + let tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + + signal_self(Signal::SIGTERM).await; + + assert!(!arbiter.fast_shutdown.is_cancelled()); + assert!(arbiter.graceful_shutdown.is_cancelled()); + assert!(arbiter.shutdown.is_cancelled()); + assert_eq!(tasks.run().await.len(), 0); + } + + #[tokio::test] + async fn signals_other_no_listener() { + let tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + + signal_self(Signal::SIGUSR1).await; + signal_self(Signal::SIGUSR2).await; + + arbiter.do_fast_shutdown().await; + assert_eq!(tasks.run().await.len(), 0); + } + + #[tokio::test] + async fn signals_usr1() { + let tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + let mut events_rx = arbiter.events_subscribe(); + + signal_self(Signal::SIGUSR1).await; + + assert_eq!( + events_rx.recv().await.expect("failed to receive event"), + Event::Signal(SignalKind::user_defined1()) + ); + } + + #[tokio::test] + async fn signals_usr2() { + let tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + let mut events_rx = arbiter.events_subscribe(); + + signal_self(Signal::SIGUSR2).await; + + assert_eq!( + events_rx.recv().await.expect("failed to receive event"), + Event::Signal(SignalKind::user_defined2()), + ); + } + + #[tokio::test] + async fn events() { + let tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + let mut events_rx1 = arbiter.events_subscribe(); + let mut events_rx2 = arbiter.events_subscribe(); + + let _ = arbiter.send_event(Event::Noop); + + assert_eq!( + events_rx1.recv().await.expect("failed to receive event"), + Event::Noop, + ); + assert_eq!( + events_rx2.recv().await.expect("failed to receive event"), + Event::Noop, + ); + } + } + + mod tasks { + use super::super::*; + + use eyre::eyre; + + async fn success_task(arbiter: Arbiter) -> Result<()> { + tokio::select! { + () = arbiter.fast_shutdown() => {}, + () = arbiter.graceful_shutdown() => {}, + } + Ok(()) + } + + async fn error_task(arbiter: Arbiter) -> Result<()> { + arbiter.shutdown().await; + Err(eyre!("error")) + } + + #[tokio::test] + async fn successful_tasks() { + let mut tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + + for _ in 0..10_u8 { + tasks + .build_task() + .spawn(success_task(arbiter.clone())) + .expect("failed to spawn task"); + } + arbiter.do_fast_shutdown().await; + + assert_eq!(tasks.run().await.len(), 0); + } + + #[tokio::test] + async fn error_tasks() { + let mut tasks = Tasks::new().expect("tasks to create successfully"); + let arbiter = tasks.arbiter(); + + for _ in 0..10_u8 { + tasks + .build_task() + .spawn(error_task(arbiter.clone())) + .expect("failed to spawn task"); + } + arbiter.do_fast_shutdown().await; + + assert_eq!(tasks.run().await.len(), 10); + } + } +} diff --git a/website/scripts/docsmg/src/hackyfixes.rs b/website/scripts/docsmg/src/hackyfixes.rs index 5fd4c6dbcb1a..88f2e8f4aded 100644 --- a/website/scripts/docsmg/src/hackyfixes.rs +++ b/website/scripts/docsmg/src/hackyfixes.rs @@ -22,9 +22,3 @@ pub(crate) fn add_extra_dot_dot_to_expression_mdx(migrate_path: &Path) { let _ = write(file, content.replace("../expressions", "../../expressions")); } } - -#[cfg(test)] -mod tests { - #[test] - fn noop() {} -} From 64b939126fc12e0af544560a194ba979beab9de0 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 17:49:16 +0200 Subject: [PATCH 04/36] lint Signed-off-by: Marc 'risson' Schmitt --- packages/ak-arbiter/src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ak-arbiter/src/lib.rs b/packages/ak-arbiter/src/lib.rs index 17004c6bd40e..03648829e354 100644 --- a/packages/ak-arbiter/src/lib.rs +++ b/packages/ak-arbiter/src/lib.rs @@ -306,9 +306,10 @@ impl From for Event { #[cfg(test)] mod tests { mod events { - use super::super::*; use nix::sys::signal::{Signal, raise}; + use super::super::*; + async fn signal_self(signal: Signal) { raise(signal).expect("failed to send signal"); tokio::time::sleep(Duration::from_millis(50)).await; @@ -427,10 +428,10 @@ mod tests { } mod tasks { - use super::super::*; - use eyre::eyre; + use super::super::*; + async fn success_task(arbiter: Arbiter) -> Result<()> { tokio::select! { () = arbiter.fast_shutdown() => {}, From 1e5cb4b8696f0ad8f1c6f172f41e064427d1d13a Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 19:32:18 +0200 Subject: [PATCH 05/36] sort out package versions Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 2 +- Cargo.toml | 4 ++++ Makefile | 1 + packages/ak-arbiter/Cargo.toml | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf6080a3e7c1..d8a0d2652570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,7 +95,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "authentik-arbiter" -version = "0.0.0" +version = "2026.5.0-rc1" dependencies = [ "axum-server", "eyre", diff --git a/Cargo.toml b/Cargo.toml index beb3bdeaa231..5dca25da60bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,10 @@ tracing = "= 0.1.44" url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } +arbiter = { package = "authentik-arbiter", version = "2026.5.0-rc1", path = "./packages/ak-arbiter" } + +authentik-client = { version = "2026.5.0-rc1", path = "./packages/client-rust" } + [profile.dev.package.backtrace] opt-level = 3 diff --git a/Makefile b/Makefile index 6d7bbfe1316f..5ca965eb9b0a 100644 --- a/Makefile +++ b/Makefile @@ -153,6 +153,7 @@ endif $(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION)) $(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' ${PWD}/pyproject.toml $(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' ${PWD}/authentik/__init__.py + $(SED_INPLACE) "s/version = \"${current_version}\"/version = \"$(version)\"" ${PWD}/Cargo.toml ${PWD}/Cargo.lock $(MAKE) gen-build gen-compose aws-cfn $(SED_INPLACE) "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json echo -n $(version) > ${PWD}/internal/constants/VERSION diff --git a/packages/ak-arbiter/Cargo.toml b/packages/ak-arbiter/Cargo.toml index 61e3593d1af3..25f9b8224c8e 100644 --- a/packages/ak-arbiter/Cargo.toml +++ b/packages/ak-arbiter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "authentik-arbiter" -version = "0.0.0" +version.workspace = true authors.workspace = true edition.workspace = true readme.workspace = true From e7d37046724ba80c0d0e61a499e0f65c10f3a475 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 19:32:59 +0200 Subject: [PATCH 06/36] packages/ak-config: init Signed-off-by: Marc 'risson' Schmitt --- .cargo/deny.toml | 2 + Cargo.lock | 209 ++++++++++++++- Cargo.toml | 10 + authentik/lib/default.yml | 14 + packages/ak-arbiter/src/lib.rs | 10 +- packages/ak-config/Cargo.toml | 30 +++ packages/ak-config/src/lib.rs | 427 +++++++++++++++++++++++++++++++ packages/ak-config/src/schema.rs | 90 +++++++ 8 files changed, 779 insertions(+), 13 deletions(-) create mode 100644 packages/ak-config/Cargo.toml create mode 100644 packages/ak-config/src/lib.rs create mode 100644 packages/ak-config/src/schema.rs diff --git a/.cargo/deny.toml b/.cargo/deny.toml index 485f23500d47..a8bcc1b1d26d 100644 --- a/.cargo/deny.toml +++ b/.cargo/deny.toml @@ -2,12 +2,14 @@ allow = [ "Apache-2.0", "BSD-3-Clause", + "CC0-1.0", "CDLA-Permissive-2.0", "ISC", "MIT", "MPL-2.0", "OpenSSL", "Unicode-3.0", + "Zlib", ] [licenses.private] diff --git a/Cargo.lock b/Cargo.lock index d8a0d2652570..2a17e0286597 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "async-trait" version = "0.1.89" @@ -123,6 +129,24 @@ dependencies = [ "uuid", ] +[[package]] +name = "authentik-config" +version = "0.0.0" +dependencies = [ + "arc-swap", + "authentik-arbiter", + "config", + "eyre", + "glob", + "notify", + "serde", + "serde_json", + "tempfile", + "tokio", + "tracing", + "url", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -201,7 +225,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools", @@ -215,6 +239,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -367,6 +397,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "config" +version = "0.15.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" +dependencies = [ + "async-trait", + "pathdiff", + "serde_core", + "winnow", + "yaml-rust2", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -477,6 +520,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -520,6 +569,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -660,6 +718,15 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -907,6 +974,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags 2.11.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1008,6 +1095,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1030,6 +1137,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -1092,6 +1205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -1102,7 +1216,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -1118,6 +1232,33 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -1174,6 +1315,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1347,7 +1494,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -1459,6 +1606,19 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.37" @@ -1577,7 +1737,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -1773,7 +1933,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -1788,6 +1948,19 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1949,7 +2122,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -2219,7 +2392,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -2520,6 +2693,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -2578,7 +2760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", @@ -2614,6 +2796,17 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 5dca25da60bc..e932c251506a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "packages/ak-arbiter", + "packages/ak-config", "packages/client-rust", "website/scripts/docsmg", ] @@ -18,13 +19,20 @@ license-file = "LICENSE" publish = false [workspace.dependencies] +arc-swap = "= 1.9.0" axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] } aws-lc-rs = { version = "= 1.16.2", features = ["fips"] } clap = { version = "= 4.6.0", features = ["derive", "env"] } colored = "= 3.1.1" +config-rs = { package = "config", version = "= 0.15.22", default-features = false, features = [ + "yaml", + "async", +] } dotenvy = "= 0.15.7" eyre = "= 0.6.12" +glob = "= 0.3.3" nix = { version = "= 0.31.2", features = ["signal"] } +notify = "= 8.2.0" regex = "= 1.12.3" reqwest = { version = "= 0.13.2", features = [ "form", @@ -48,6 +56,7 @@ serde_repr = "= 0.1.20" serde_with = { version = "= 3.18.0", default-features = false, features = [ "base64", ] } +tempfile = "= 3.27.0" tokio = { version = "= 1.50.0", features = ["full", "tracing"] } tokio-util = { version = "= 0.7.18", features = ["full"] } tracing = "= 0.1.44" @@ -55,6 +64,7 @@ url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } arbiter = { package = "authentik-arbiter", version = "2026.5.0-rc1", path = "./packages/ak-arbiter" } +config = { package = "authentik-config", version = "2026.5.0-rc1", path = "./packages/ak-config" } authentik-client = { version = "2026.5.0-rc1", path = "./packages/client-rust" } diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index c730502bab21..cfbb9c68ba8a 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -47,6 +47,7 @@ listen: - "[::]:9300" debug: 0.0.0.0:9900 debug_py: 0.0.0.0:9901 + debug_tokio: "[::]:6669" trusted_proxy_cidrs: - 127.0.0.0/8 - 10.0.0.0/8 @@ -73,6 +74,19 @@ log_level: info log: http_headers: - User-Agent + rust_log: + "console_subscriber": info + "h2": info + "hyper_util": warn + "mio": info + "notify": info + "reqwest": info + "runtime": info + "rustls": info + "sqlx": info + "sqlx_postgres": info + "tokio": info + "tungstenite": info sessions: unauthenticated_age: days=1 diff --git a/packages/ak-arbiter/src/lib.rs b/packages/ak-arbiter/src/lib.rs index 03648829e354..43b571b8f931 100644 --- a/packages/ak-arbiter/src/lib.rs +++ b/packages/ak-arbiter/src/lib.rs @@ -293,8 +293,8 @@ impl Arbiter { pub enum Event { /// A signal has been received. Signal(SignalKind), - #[cfg(test)] - Noop, + /// The configuration has been reloaded from sources. + ConfigChanged, } impl From for Event { @@ -414,15 +414,15 @@ mod tests { let mut events_rx1 = arbiter.events_subscribe(); let mut events_rx2 = arbiter.events_subscribe(); - let _ = arbiter.send_event(Event::Noop); + let _ = arbiter.send_event(Event::ConfigChanged); assert_eq!( events_rx1.recv().await.expect("failed to receive event"), - Event::Noop, + Event::ConfigChanged, ); assert_eq!( events_rx2.recv().await.expect("failed to receive event"), - Event::Noop, + Event::ConfigChanged, ); } } diff --git a/packages/ak-config/Cargo.toml b/packages/ak-config/Cargo.toml new file mode 100644 index 000000000000..453edcc15068 --- /dev/null +++ b/packages/ak-config/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "authentik-config" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +publish.workspace = true + +[dependencies] +arc-swap.workspace = true +config-rs.workspace = true +eyre.workspace = true +glob.workspace = true +notify.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +tracing.workspace = true +url.workspace = true + +arbiter.workspace = true + +[dev-dependencies] +tempfile.workspace = true + +[lints] +workspace = true diff --git a/packages/ak-config/src/lib.rs b/packages/ak-config/src/lib.rs new file mode 100644 index 000000000000..e736e6ac908b --- /dev/null +++ b/packages/ak-config/src/lib.rs @@ -0,0 +1,427 @@ +use std::{ + env, + fs::{self, read_to_string}, + path::PathBuf, + sync::{Arc, OnceLock}, +}; + +use arc_swap::ArcSwap; +use eyre::Result; +use notify::{RecommendedWatcher, Watcher as _}; +use serde_json::{Map, Value}; +use tokio::sync::mpsc; +use tracing::{error, info, warn}; +use url::Url; + +pub mod schema; +use arbiter::{Arbiter, Event, Tasks}; +pub use schema::Config; + +static DEFAULT_CONFIG: &str = include_str!("../../../authentik/lib/default.yml"); +static CONFIG_MANAGER: OnceLock = OnceLock::new(); + +/// List of paths from where to read YAML configuration. +fn config_paths() -> Vec { + let mut config_paths = vec![ + PathBuf::from("/etc/authentik/config.yml"), + PathBuf::from(""), + ]; + if let Ok(workspace) = env::var("WORKSPACE_DIR") { + let _ = env::set_current_dir(workspace); + } + + if let Ok(paths) = glob::glob("/etc/authentik/config.d/*.yml") { + config_paths.extend(paths.filter_map(Result::ok)); + } + + let environment = env::var("AUTHENTIK_ENV").unwrap_or_else(|_| "local".to_owned()); + + let mut computed_paths = Vec::new(); + + for path in config_paths { + if let Ok(metadata) = fs::metadata(&path) { + if !metadata.is_dir() { + computed_paths.push(path); + } + } else { + let env_paths = vec![ + path.join(format!("{environment}.yml")), + path.join(format!("{environment}.env.yml")), + ]; + for env_path in env_paths { + if let Ok(metadata) = fs::metadata(&env_path) + && !metadata.is_dir() + { + computed_paths.push(env_path); + } + } + } + } + + computed_paths +} + +impl Config { + /// Load the configuration from files and environment into a [`Value`], allowing for extra + /// processing later. + fn load_raw(config_paths: &[PathBuf]) -> Result { + let mut builder = config_rs::Config::builder().add_source(config_rs::File::from_str( + DEFAULT_CONFIG, + config_rs::FileFormat::Yaml, + )); + for path in config_paths { + builder = builder.add_source( + config_rs::File::from(path.as_path()).format(config_rs::FileFormat::Yaml), + ); + } + builder = builder.add_source( + config_rs::Environment::with_prefix("AUTHENTIK") + .prefix_separator("_") + .separator("__"), + ); + let config = builder.build()?; + let raw = config.try_deserialize::()?; + Ok(raw) + } + + /// Expand a value if it matches an env:// or file:// protocol. + /// + /// If expanded from a file, returns the file path for it to be watched later. + fn expand_value(value: &str) -> (String, Option) { + let value = value.trim(); + if let Ok(uri) = Url::parse(value) { + let fallback = uri.query().unwrap_or("").to_owned(); + match uri.scheme() { + "file" => { + let path = uri.path(); + match read_to_string(path).map(|s| s.trim().to_owned()) { + Ok(value) => return (value, Some(PathBuf::from(path))), + Err(err) => { + error!("failed to read config value from {path}: {err}"); + return (fallback, Some(PathBuf::from(path))); + } + } + } + "env" => { + if let Some(var) = uri.host_str() { + if let Ok(value) = env::var(var) { + return (value, None); + } + return (fallback, None); + } + } + _ => {} + } + } + + (value.to_owned(), None) + } + + /// Expand the configuration for env:// and file:// values. + /// + /// Returns the expanded configuration and a list of file paths for which to watch changes. + fn expand(mut raw: Value) -> (Value, Vec) { + let mut file_paths = Vec::new(); + let value = match &mut raw { + Value::String(s) => { + let (v, path) = Self::expand_value(s); + if let Some(path) = path { + file_paths.push(path); + } + Value::String(v) + } + Value::Array(arr) => { + let mut res = Vec::with_capacity(arr.len()); + for v in arr { + let (expanded, paths) = Self::expand(v.clone()); + file_paths.extend(paths); + res.push(expanded); + } + Value::Array(res) + } + Value::Object(map) => { + let mut res = Map::with_capacity(map.len()); + for (k, v) in map { + let (expanded, paths) = Self::expand(v.clone()); + file_paths.extend(paths); + res.insert(k.clone(), expanded); + } + Value::Object(res) + } + _ => raw, + }; + (value, file_paths) + } + + /// Load the configuration. + fn load(config_paths: &[PathBuf]) -> Result<(Self, Vec)> { + let raw = Self::load_raw(config_paths)?; + let (expanded, file_paths) = Self::expand(raw); + let config: Self = serde_json::from_value(expanded)?; + Ok((config, file_paths)) + } +} + +/// Manager of the config. Handles reloading when changed on disk. +struct ConfigManager { + config: ArcSwap, + config_paths: Vec, + watch_paths: Vec, +} + +/// Initialize the configuration. It relies on a global [`OnceLock`] and must be called before +/// other methods are called. +pub fn init() -> Result<()> { + info!("loading config"); + let config_paths = config_paths(); + init_with_paths(config_paths)?; + info!("config loaded"); + Ok(()) +} + +/// Initialize the configuration from a list of specific paths to read if from. +fn init_with_paths(config_paths: Vec) -> Result<()> { + let (config, mut other_paths) = Config::load(&config_paths)?; + let mut watch_paths = config_paths.clone(); + watch_paths.append(&mut other_paths); + let manager = ConfigManager { + config: ArcSwap::from_pointee(config), + config_paths, + watch_paths, + }; + CONFIG_MANAGER.get_or_init(|| manager); + Ok(()) +} + +/// Watch for configuration changes, reload the configuration in memory and send events. +/// +/// [`init`] must be called before this is used. +async fn watch_config(arbiter: Arbiter) -> Result<()> { + let (tx, mut rx) = mpsc::channel(100); + let mut watcher = RecommendedWatcher::new( + move |res: notify::Result| { + if let Ok(event) = res + && let notify::EventKind::Modify(_) = &event.kind + { + let _ = tx.blocking_send(()); + } + }, + notify::Config::default(), + )?; + let watch_paths = &CONFIG_MANAGER + .get() + .expect("failed to get config, has it been initialized?") + .watch_paths; + for path in watch_paths { + watcher.watch(path.as_ref(), notify::RecursiveMode::NonRecursive)?; + } + + let _ = arbiter.send_event(Event::ConfigChanged); + info!("config file watcher started on paths: {:?}", watch_paths); + + loop { + tokio::select! { + res = rx.recv() => { + info!("a configuration file changed, reloading config"); + if res.is_none() { + break; + } + let manager = CONFIG_MANAGER.get().expect("failed to get config, has it been initialized?"); + match tokio::task::spawn_blocking(|| Config::load(&manager.config_paths)).await? { + Ok((new_config, _)) => { + info!("configuration reloaded"); + manager.config.store(Arc::new(new_config)); + if let Err(err) = arbiter.send_event(Event::ConfigChanged) { + warn!("failed to notify of config change, aborting: {err:?}"); + break; + } + } + Err(err) => { + warn!("failed to reload config, continuing with previous config: {err:?}"); + } + } + }, + () = arbiter.shutdown() => break, + } + } + + info!("stopping config file watcher"); + + Ok(()) +} + +/// Start the configuration watcher. +/// +/// [`init`] must be called before this is used. +pub fn run(tasks: &mut Tasks) -> Result<()> { + info!("starting config file watcher"); + let arbiter = tasks.arbiter(); + tasks + .build_task() + .name(&format!("{}::watch_config", module_path!())) + .spawn(watch_config(arbiter))?; + Ok(()) +} + +/// Get the currently stored configuration. +/// +/// [`init`] must be called before this is used. +pub fn get() -> arc_swap::Guard> { + let manager = CONFIG_MANAGER + .get() + .expect("failed to get config, has it been initialized?"); + manager.config.load() +} + +#[cfg(test)] +mod tests { + use std::{env, fs::File, io::Write as _, path::PathBuf}; + + use arbiter::{Event, Tasks}; + use tempfile::tempdir; + + #[test] + fn default_config() { + let (config, _) = super::Config::load(&[]).expect("default config doesn't load"); + assert_eq!(config.secret_key, ""); + } + + #[test] + fn config_paths() { + let temp_dir = tempdir().expect("failed to create temp dir"); + for f in &[ + "local.env.yml", + "local.env.yaml", + "test_config_paths.yml", + "test_config_paths.env.yml", + "test_config_paths.env.yaml", + ] { + File::create(temp_dir.path().join(f)).expect("failed to create file"); + } + #[expect(unsafe_code, reason = "testing")] + // SAFETY: testing + unsafe { + env::set_var("WORKSPACE_DIR", temp_dir.path()); + env::set_var("AUTHENTIK_ENV", "test_config_paths"); + } + + let paths = super::config_paths(); + + assert_eq!( + &paths, + &[ + PathBuf::from("test_config_paths.yml"), + PathBuf::from("test_config_paths.env.yml"), + ] + ); + } + + #[test] + fn expand() { + let temp_dir = tempdir().expect("failed to create temp dir"); + + let secret_key_path = temp_dir.path().join("secret_key"); + let mut secret_key_file = File::create(&secret_key_path).expect("failed to create file"); + write!(secret_key_file, "my_secret_key").expect("failed to write to file"); + + let config_file_path = temp_dir.path().join("config"); + let mut config_file = File::create(&config_file_path).expect("failed to create file"); + writeln!( + config_file, + "secret_key: file://{}\npostgresql:\n password: env://TEST_CONFIG_POSTGRES_PASS", + secret_key_path.display() + ) + .expect("failed to write to file"); + + #[expect(unsafe_code, reason = "testing")] + // SAFETY: testing + unsafe { + env::set_var("TEST_CONFIG_POSTGRES_PASS", "my_postgres_pass"); + } + + let (config, config_paths) = + super::Config::load(&[config_file_path]).expect("failed to load config"); + + assert_eq!(config.secret_key, "my_secret_key"); + assert_eq!(config.postgresql.password, "my_postgres_pass"); + assert_eq!(config_paths, &[secret_key_path]); + } + + #[test] + fn init() { + super::init_with_paths(vec![]).expect("failed to init config"); + } + + #[tokio::test] + async fn watcher() { + let temp_dir = tempdir().expect("failed to create temp dir"); + + let secret_key_path = temp_dir.path().join("secret_key"); + let mut secret_key_file = File::create(&secret_key_path).expect("failed to create file"); + write!(secret_key_file, "my_secret_key").expect("failed to write to file"); + drop(secret_key_file); + + let config_file_path = temp_dir.path().join("config"); + let mut config_file = File::create(&config_file_path).expect("failed to create file"); + writeln!( + config_file, + "secret_key: file://{}\npostgresql:\n password: my_postgres_pass", + secret_key_path.display() + ) + .expect("failed to write to file"); + drop(config_file); + + super::init_with_paths(vec![config_file_path.clone()]).expect("failed to init config"); + + let mut tasks = Tasks::new().expect("failed to create tasks"); + let arbiter = tasks.arbiter(); + let mut events_rx = arbiter.events_subscribe(); + + super::run(&mut tasks).expect("failed to start watcher"); + + assert_eq!(super::get().secret_key, "my_secret_key"); + assert_eq!(super::get().postgresql.password, "my_postgres_pass"); + + let _ = events_rx.recv().await; + let mut secret_key_file = File::create(&secret_key_path).expect("failed to open file"); + write!(secret_key_file, "my_other_secret_key").expect("failed to write to file"); + drop(secret_key_file); + + assert_eq!( + events_rx.recv().await.expect("failed to receive event"), + Event::ConfigChanged, + ); + while !events_rx.is_empty() { + assert_eq!( + events_rx.recv().await.expect("failed to receive event"), + Event::ConfigChanged, + ); + } + + assert_eq!(super::get().secret_key, "my_other_secret_key"); + assert_eq!(super::get().postgresql.password, "my_postgres_pass"); + + let mut config_file = File::create(&config_file_path).expect("failed to open file"); + writeln!( + config_file, + "secret_key: file://{}\npostgresql:\n password: my_new_postgres_pass", + secret_key_path.display() + ) + .expect("failed to write to file"); + drop(config_file); + + assert_eq!( + events_rx.recv().await.expect("failed to receive event"), + Event::ConfigChanged, + ); + while !events_rx.is_empty() { + assert_eq!( + events_rx.recv().await.expect("failed to receive event"), + Event::ConfigChanged, + ); + } + + assert_eq!(super::get().secret_key, "my_other_secret_key"); + assert_eq!(super::get().postgresql.password, "my_new_postgres_pass"); + } +} diff --git a/packages/ak-config/src/schema.rs b/packages/ak-config/src/schema.rs new file mode 100644 index 000000000000..29b945c27e53 --- /dev/null +++ b/packages/ak-config/src/schema.rs @@ -0,0 +1,90 @@ +use std::{collections::HashMap, net::SocketAddr, num::NonZeroUsize}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub postgresql: PostgreSQLConfig, + + pub listen: ListenConfig, + + pub debug: bool, + #[serde(default)] + pub secret_key: String, + + pub log_level: String, + pub log: LogConfig, + + pub error_reporting: ErrorReportingConfig, + + pub compliance: ComplianceConfig, + + pub web: WebConfig, + + pub worker: WorkerConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PostgreSQLConfig { + pub host: String, + pub port: u16, + pub user: String, + pub password: String, + pub name: String, + + pub sslmode: String, + pub sslrootcert: Option, + pub sslcert: Option, + pub sslkey: Option, + + pub conn_max_age: Option, + pub conn_health_checks: bool, + + pub default_schema: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListenConfig { + pub http: Vec, + pub metrics: Vec, + pub debug_tokio: SocketAddr, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogConfig { + pub http_headers: Vec, + pub rust_log: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ErrorReportingConfig { + pub enabled: bool, + pub sentry_dsn: Option, + pub environment: String, + pub send_pii: bool, + pub sample_rate: f32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComplianceConfig { + pub fips: ComplianceFipsConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComplianceFipsConfig { + pub enabled: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebConfig { + pub path: String, + pub timeout_http_read_header: String, + pub timeout_http_read: String, + pub timeout_http_write: String, + pub timeout_http_idle: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkerConfig { + pub processes: NonZeroUsize, +} From 27ff03943c8b50ca9cbd5f5ca0cb6a1557b3f84c Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 19:40:37 +0200 Subject: [PATCH 07/36] rename to ak-lib Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 24 +++++++++---------- Cargo.toml | 8 ++----- packages/{ak-arbiter => ak-lib}/Cargo.toml | 2 +- .../src/lib.rs => ak-lib/src/arbiter.rs} | 0 packages/ak-lib/src/lib.rs | 4 ++++ 5 files changed, 19 insertions(+), 19 deletions(-) rename packages/{ak-arbiter => ak-lib}/Cargo.toml (94%) rename packages/{ak-arbiter/src/lib.rs => ak-lib/src/arbiter.rs} (100%) create mode 100644 packages/ak-lib/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d8a0d2652570..5affeda966cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,18 +93,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "authentik-arbiter" -version = "2026.5.0-rc1" -dependencies = [ - "axum-server", - "eyre", - "nix", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "authentik-client" version = "2026.5.0-rc1" @@ -123,6 +111,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "authentik-lib" +version = "2026.5.0-rc1" +dependencies = [ + "axum-server", + "eyre", + "nix", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "autocfg" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 5dca25da60bc..c652d529944d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,5 @@ [workspace] -members = [ - "packages/ak-arbiter", - "packages/client-rust", - "website/scripts/docsmg", -] +members = ["packages/ak-lib", "packages/client-rust", "website/scripts/docsmg"] resolver = "3" [workspace.package] @@ -54,7 +50,7 @@ tracing = "= 0.1.44" url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } -arbiter = { package = "authentik-arbiter", version = "2026.5.0-rc1", path = "./packages/ak-arbiter" } +lib = { package = "authentik-lib", version = "2026.5.0-rc1", path = "./packages/ak-lib" } authentik-client = { version = "2026.5.0-rc1", path = "./packages/client-rust" } diff --git a/packages/ak-arbiter/Cargo.toml b/packages/ak-lib/Cargo.toml similarity index 94% rename from packages/ak-arbiter/Cargo.toml rename to packages/ak-lib/Cargo.toml index 25f9b8224c8e..20ac01b28141 100644 --- a/packages/ak-arbiter/Cargo.toml +++ b/packages/ak-lib/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "authentik-arbiter" +name = "authentik-lib" version.workspace = true authors.workspace = true edition.workspace = true diff --git a/packages/ak-arbiter/src/lib.rs b/packages/ak-lib/src/arbiter.rs similarity index 100% rename from packages/ak-arbiter/src/lib.rs rename to packages/ak-lib/src/arbiter.rs diff --git a/packages/ak-lib/src/lib.rs b/packages/ak-lib/src/lib.rs new file mode 100644 index 000000000000..e8c873e42755 --- /dev/null +++ b/packages/ak-lib/src/lib.rs @@ -0,0 +1,4 @@ +//! Various utilities used by other crates + +pub mod arbiter; +pub use arbiter::{Arbiter, Tasks}; From 1ee6f119e076d4a7a1781ebf748b8c922501988b Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 19:43:21 +0200 Subject: [PATCH 08/36] fixup Signed-off-by: Marc 'risson' Schmitt --- packages/ak-lib/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ak-lib/src/lib.rs b/packages/ak-lib/src/lib.rs index e8c873e42755..45ce72a13eb1 100644 --- a/packages/ak-lib/src/lib.rs +++ b/packages/ak-lib/src/lib.rs @@ -1,4 +1,4 @@ //! Various utilities used by other crates pub mod arbiter; -pub use arbiter::{Arbiter, Tasks}; +pub use arbiter::{Arbiter, Event, Tasks}; From d7141df7f6b1a727d2f734e3b540f2062cf58ddf Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 19:48:34 +0200 Subject: [PATCH 09/36] fixup Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 2 +- packages/ak-config/Cargo.toml | 2 +- packages/ak-config/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ce9b761e0ca..65d124d6f33b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,7 +119,7 @@ dependencies = [ [[package]] name = "authentik-config" -version = "0.0.0" +version = "2026.5.0-rc1" dependencies = [ "arc-swap", "authentik-lib", diff --git a/packages/ak-config/Cargo.toml b/packages/ak-config/Cargo.toml index 3648ed95fc53..dcd82a5703e4 100644 --- a/packages/ak-config/Cargo.toml +++ b/packages/ak-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "authentik-config" -version = "0.0.0" +version.workspace = true authors.workspace = true edition.workspace = true readme.workspace = true diff --git a/packages/ak-config/src/lib.rs b/packages/ak-config/src/lib.rs index 2f2069054608..91654893efd4 100644 --- a/packages/ak-config/src/lib.rs +++ b/packages/ak-config/src/lib.rs @@ -277,7 +277,7 @@ pub fn get() -> arc_swap::Guard> { mod tests { use std::{env, fs::File, io::Write as _, path::PathBuf}; - use arbiter::{Event, Tasks}; + use lib::{Event, Tasks}; use tempfile::tempdir; #[test] From 4c54511b38463736bbad2d57cf8b3a8b1c5d954d Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 19:52:55 +0200 Subject: [PATCH 10/36] move into lib crate Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 18 +++-------- Cargo.toml | 8 +---- packages/ak-config/Cargo.toml | 30 ------------------- packages/ak-lib/Cargo.toml | 8 +++++ .../src/lib.rs => ak-lib/src/config/mod.rs} | 6 ++-- .../src => ak-lib/src/config}/schema.rs | 0 packages/ak-lib/src/lib.rs | 1 + 7 files changed, 17 insertions(+), 54 deletions(-) delete mode 100644 packages/ak-config/Cargo.toml rename packages/{ak-config/src/lib.rs => ak-lib/src/config/mod.rs} (98%) rename packages/{ak-config/src => ak-lib/src/config}/schema.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 65d124d6f33b..9d3e6b22056f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,33 +118,23 @@ dependencies = [ ] [[package]] -name = "authentik-config" +name = "authentik-lib" version = "2026.5.0-rc1" dependencies = [ "arc-swap", - "authentik-lib", + "axum-server", "config", "eyre", "glob", + "nix", "notify", "serde", "serde_json", "tempfile", "tokio", - "tracing", - "url", -] - -[[package]] -name = "authentik-lib" -version = "2026.5.0-rc1" -dependencies = [ - "axum-server", - "eyre", - "nix", - "tokio", "tokio-util", "tracing", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e95e849b9e86..fac9c8031dd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,5 @@ [workspace] -members = [ - "packages/ak-lib", - "packages/ak-config", - "packages/client-rust", - "website/scripts/docsmg", -] +members = ["packages/ak-lib", "packages/client-rust", "website/scripts/docsmg"] resolver = "3" [workspace.package] @@ -63,7 +58,6 @@ tracing = "= 0.1.44" url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } -config = { package = "authentik-config", version = "2026.5.0-rc1", path = "./packages/ak-config" } lib = { package = "authentik-lib", version = "2026.5.0-rc1", path = "./packages/ak-lib" } authentik-client = { version = "2026.5.0-rc1", path = "./packages/client-rust" } diff --git a/packages/ak-config/Cargo.toml b/packages/ak-config/Cargo.toml deleted file mode 100644 index dcd82a5703e4..000000000000 --- a/packages/ak-config/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "authentik-config" -version.workspace = true -authors.workspace = true -edition.workspace = true -readme.workspace = true -homepage.workspace = true -repository.workspace = true -license-file.workspace = true -publish.workspace = true - -[dependencies] -arc-swap.workspace = true -config-rs.workspace = true -eyre.workspace = true -glob.workspace = true -notify.workspace = true -serde.workspace = true -serde_json.workspace = true -tokio.workspace = true -tracing.workspace = true -url.workspace = true - -lib.workspace = true - -[dev-dependencies] -tempfile.workspace = true - -[lints] -workspace = true diff --git a/packages/ak-lib/Cargo.toml b/packages/ak-lib/Cargo.toml index 20ac01b28141..e36cbaf05ad4 100644 --- a/packages/ak-lib/Cargo.toml +++ b/packages/ak-lib/Cargo.toml @@ -10,14 +10,22 @@ license-file.workspace = true publish.workspace = true [dependencies] +arc-swap.workspace = true axum-server.workspace = true +config-rs.workspace = true eyre.workspace = true +glob.workspace = true +notify.workspace = true +serde.workspace = true +serde_json.workspace = true tokio.workspace = true tokio-util.workspace = true tracing.workspace = true +url.workspace = true [dev-dependencies] nix.workspace = true +tempfile.workspace = true [lints] workspace = true diff --git a/packages/ak-config/src/lib.rs b/packages/ak-lib/src/config/mod.rs similarity index 98% rename from packages/ak-config/src/lib.rs rename to packages/ak-lib/src/config/mod.rs index 91654893efd4..b12a99a3f6ef 100644 --- a/packages/ak-config/src/lib.rs +++ b/packages/ak-lib/src/config/mod.rs @@ -14,10 +14,10 @@ use tracing::{error, info, warn}; use url::Url; pub mod schema; -use lib::{Arbiter, Event, Tasks}; +use crate::arbiter::{Arbiter, Event, Tasks}; pub use schema::Config; -static DEFAULT_CONFIG: &str = include_str!("../../../authentik/lib/default.yml"); +static DEFAULT_CONFIG: &str = include_str!("../../../../authentik/lib/default.yml"); static CONFIG_MANAGER: OnceLock = OnceLock::new(); /// List of paths from where to read YAML configuration. @@ -277,7 +277,7 @@ pub fn get() -> arc_swap::Guard> { mod tests { use std::{env, fs::File, io::Write as _, path::PathBuf}; - use lib::{Event, Tasks}; + use crate::arbiter::{Event, Tasks}; use tempfile::tempdir; #[test] diff --git a/packages/ak-config/src/schema.rs b/packages/ak-lib/src/config/schema.rs similarity index 100% rename from packages/ak-config/src/schema.rs rename to packages/ak-lib/src/config/schema.rs diff --git a/packages/ak-lib/src/lib.rs b/packages/ak-lib/src/lib.rs index 45ce72a13eb1..7e226ab2c2c3 100644 --- a/packages/ak-lib/src/lib.rs +++ b/packages/ak-lib/src/lib.rs @@ -2,3 +2,4 @@ pub mod arbiter; pub use arbiter::{Arbiter, Event, Tasks}; +pub mod config; From dc65ab105bdde0843415acc6918b79171ca2e54a Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 16:33:49 +0200 Subject: [PATCH 11/36] packages/ak-lib: init Signed-off-by: Marc 'risson' Schmitt --- .cargo/config.toml | 5 +++ .dockerignore | 2 +- CODEOWNERS | 1 + Cargo.lock | 85 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 22 ++++++++-- Makefile | 1 + packages/ak-lib/Cargo.toml | 23 +++++++++++ packages/ak-lib/src/lib.rs | 20 +++++++++ 8 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 packages/ak-lib/Cargo.toml create mode 100644 packages/ak-lib/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000000..29b17fd88bb1 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[alias] +t = ["nextest", "run"] + +[build] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/.dockerignore b/.dockerignore index 75c223bfdcfa..f8e411e9bb21 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,4 +10,4 @@ build_docs/** blueprints/local .git .venv -target/ +target diff --git a/CODEOWNERS b/CODEOWNERS index 74346050246c..01971905f207 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -27,6 +27,7 @@ Makefile @goauthentik/infrastructure .editorconfig @goauthentik/infrastructure CODEOWNERS @goauthentik/infrastructure # Backend packages +packages/ak-* @goauthentik/backend packages/client-rust @goauthentik/backend packages/django-channels-postgres @goauthentik/backend packages/django-postgres-cache @goauthentik/backend diff --git a/Cargo.lock b/Cargo.lock index b6b1d8d7583e..5affeda966cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -102,6 +111,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "authentik-lib" +version = "2026.5.0-rc1" +dependencies = [ + "axum-server", + "eyre", + "nix", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -146,6 +167,28 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" @@ -461,6 +504,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -658,6 +711,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -672,6 +731,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1036,6 +1096,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1814,6 +1886,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -1907,9 +1980,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" diff --git a/Cargo.toml b/Cargo.toml index b47bfb0b50ea..c652d529944d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["packages/client-rust", "website/scripts/docsmg"] +members = ["packages/ak-lib", "packages/client-rust", "website/scripts/docsmg"] resolver = "3" [workspace.package] @@ -14,11 +14,13 @@ license-file = "LICENSE" publish = false [workspace.dependencies] +axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] } aws-lc-rs = { version = "= 1.16.2", features = ["fips"] } clap = { version = "= 4.6.0", features = ["derive", "env"] } colored = "= 3.1.1" dotenvy = "= 0.15.7" eyre = "= 0.6.12" +nix = { version = "= 0.31.2", features = ["signal"] } regex = "= 1.12.3" reqwest = { version = "= 0.13.2", features = [ "form", @@ -42,11 +44,16 @@ serde_repr = "= 0.1.20" serde_with = { version = "= 3.18.0", default-features = false, features = [ "base64", ] } -tokio = { version = "= 1.50.0", features = ["full"] } +tokio = { version = "= 1.50.0", features = ["full", "tracing"] } tokio-util = { version = "= 0.7.18", features = ["full"] } +tracing = "= 0.1.44" url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } +lib = { package = "authentik-lib", version = "2026.5.0-rc1", path = "./packages/ak-lib" } + +authentik-client = { version = "2026.5.0-rc1", path = "./packages/client-rust" } + [profile.dev.package.backtrace] opt-level = 3 @@ -89,12 +96,20 @@ perf = { priority = -1, level = "warn" } style = { priority = -1, level = "warn" } suspicious = { priority = -1, level = "warn" } ### and disable the ones we don't want +### cargo group +multiple_crate_versions = "allow" ### pedantic group +missing_errors_doc = "allow" +missing_panics_doc = "allow" +must_use_candidate = "allow" redundant_closure_for_method_calls = "allow" +struct_field_names = "allow" too_many_lines = "allow" ### nursery -redundant_pub_crate = "allow" +missing_const_for_fn = "allow" option_if_let_else = "allow" +redundant_pub_crate = "allow" +significant_drop_tightening = "allow" ### restriction group allow_attributes = "warn" allow_attributes_without_reason = "warn" @@ -107,7 +122,6 @@ create_dir = "warn" dbg_macro = "warn" default_numeric_fallback = "warn" disallowed_script_idents = "warn" -doc_paragraphs_missing_punctuation = "warn" empty_drop = "warn" empty_enum_variants_with_brackets = "warn" empty_structs_with_brackets = "warn" diff --git a/Makefile b/Makefile index 6d7bbfe1316f..5ca965eb9b0a 100644 --- a/Makefile +++ b/Makefile @@ -153,6 +153,7 @@ endif $(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION)) $(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' ${PWD}/pyproject.toml $(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' ${PWD}/authentik/__init__.py + $(SED_INPLACE) "s/version = \"${current_version}\"/version = \"$(version)\"" ${PWD}/Cargo.toml ${PWD}/Cargo.lock $(MAKE) gen-build gen-compose aws-cfn $(SED_INPLACE) "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json echo -n $(version) > ${PWD}/internal/constants/VERSION diff --git a/packages/ak-lib/Cargo.toml b/packages/ak-lib/Cargo.toml new file mode 100644 index 000000000000..20ac01b28141 --- /dev/null +++ b/packages/ak-lib/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "authentik-lib" +version.workspace = true +authors.workspace = true +edition.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +publish.workspace = true + +[dependencies] +axum-server.workspace = true +eyre.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tracing.workspace = true + +[dev-dependencies] +nix.workspace = true + +[lints] +workspace = true diff --git a/packages/ak-lib/src/lib.rs b/packages/ak-lib/src/lib.rs new file mode 100644 index 000000000000..e63d65161f55 --- /dev/null +++ b/packages/ak-lib/src/lib.rs @@ -0,0 +1,20 @@ +//! Various utilities used by other crates + +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn authentik_build_hash(fallback: Option) -> String { + std::env::var("GIT_BUILD_HASH").unwrap_or_else(|_| fallback.unwrap_or_default()) +} + +pub fn authentik_full_version() -> String { + let build_hash = authentik_build_hash(None); + if build_hash.is_empty() { + VERSION.to_owned() + } else { + format!("{VERSION}+{build_hash}") + } +} + +pub fn authentik_user_agent() -> String { + format!("authentik@{}", authentik_full_version()) +} From 524b788a3c8fc083713284420baace9c11336d43 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 19:57:51 +0200 Subject: [PATCH 12/36] fixup Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 80 -------------------------------------- Cargo.toml | 9 +++-- packages/ak-lib/Cargo.toml | 6 --- 3 files changed, 5 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5affeda966cc..c86d846a1b20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,15 +67,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arc-swap" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" -dependencies = [ - "rustversion", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -114,14 +105,6 @@ dependencies = [ [[package]] name = "authentik-lib" version = "2026.5.0-rc1" -dependencies = [ - "axum-server", - "eyre", - "nix", - "tokio", - "tokio-util", - "tracing", -] [[package]] name = "autocfg" @@ -167,28 +150,6 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "axum-server" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" -dependencies = [ - "arc-swap", - "bytes", - "either", - "fs-err", - "http", - "http-body", - "hyper", - "hyper-util", - "pin-project-lite", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - [[package]] name = "base64" version = "0.22.1" @@ -504,16 +465,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs-err" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" -dependencies = [ - "autocfg", - "tokio", -] - [[package]] name = "fs_extra" version = "1.3.0" @@ -711,12 +662,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" version = "1.8.1" @@ -731,7 +676,6 @@ dependencies = [ "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1096,18 +1040,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "nix" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -1980,21 +1912,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" version = "0.1.36" diff --git a/Cargo.toml b/Cargo.toml index c652d529944d..2f8dfe6fc787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,9 @@ [workspace] -members = ["packages/ak-lib", "packages/client-rust", "website/scripts/docsmg"] +members = [ + "packages/ak-lib", + "packages/client-rust", + "website/scripts/docsmg", +] resolver = "3" [workspace.package] @@ -14,13 +18,11 @@ license-file = "LICENSE" publish = false [workspace.dependencies] -axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] } aws-lc-rs = { version = "= 1.16.2", features = ["fips"] } clap = { version = "= 4.6.0", features = ["derive", "env"] } colored = "= 3.1.1" dotenvy = "= 0.15.7" eyre = "= 0.6.12" -nix = { version = "= 0.31.2", features = ["signal"] } regex = "= 1.12.3" reqwest = { version = "= 0.13.2", features = [ "form", @@ -46,7 +48,6 @@ serde_with = { version = "= 3.18.0", default-features = false, features = [ ] } tokio = { version = "= 1.50.0", features = ["full", "tracing"] } tokio-util = { version = "= 0.7.18", features = ["full"] } -tracing = "= 0.1.44" url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } diff --git a/packages/ak-lib/Cargo.toml b/packages/ak-lib/Cargo.toml index 20ac01b28141..a935ca6288c9 100644 --- a/packages/ak-lib/Cargo.toml +++ b/packages/ak-lib/Cargo.toml @@ -10,14 +10,8 @@ license-file.workspace = true publish.workspace = true [dependencies] -axum-server.workspace = true -eyre.workspace = true -tokio.workspace = true -tokio-util.workspace = true -tracing.workspace = true [dev-dependencies] -nix.workspace = true [lints] workspace = true From 48c833c69b0180ba473efd4fbab8606152adf2f8 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 30 Mar 2026 20:31:04 +0200 Subject: [PATCH 13/36] lint Signed-off-by: Marc 'risson' Schmitt --- packages/ak-lib/src/config/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ak-lib/src/config/mod.rs b/packages/ak-lib/src/config/mod.rs index b12a99a3f6ef..e63641e32047 100644 --- a/packages/ak-lib/src/config/mod.rs +++ b/packages/ak-lib/src/config/mod.rs @@ -14,9 +14,10 @@ use tracing::{error, info, warn}; use url::Url; pub mod schema; -use crate::arbiter::{Arbiter, Event, Tasks}; pub use schema::Config; +use crate::arbiter::{Arbiter, Event, Tasks}; + static DEFAULT_CONFIG: &str = include_str!("../../../../authentik/lib/default.yml"); static CONFIG_MANAGER: OnceLock = OnceLock::new(); @@ -277,9 +278,10 @@ pub fn get() -> arc_swap::Guard> { mod tests { use std::{env, fs::File, io::Write as _, path::PathBuf}; - use crate::arbiter::{Event, Tasks}; use tempfile::tempdir; + use crate::arbiter::{Event, Tasks}; + #[test] fn default_config() { let (config, _) = super::Config::load(&[]).expect("default config doesn't load"); From 3ef00802fa7a51157efa7e092ceae012405faee4 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 16:29:35 +0200 Subject: [PATCH 14/36] packages/ak-lib/tokio/proxy_procotol: init Signed-off-by: Marc 'risson' Schmitt --- .cargo/rustfmt.toml | 2 +- Cargo.lock | 19 + Cargo.toml | 9 +- packages/ak-lib/Cargo.toml | 5 + packages/ak-lib/src/lib.rs | 2 + packages/ak-lib/src/tokio/mod.rs | 3 + .../ak-lib/src/tokio/proxy_protocol/LICENSE | 22 + .../ak-lib/src/tokio/proxy_protocol/README.md | 1 + .../ak-lib/src/tokio/proxy_protocol/header.rs | 616 ++++++++++++++++++ .../ak-lib/src/tokio/proxy_protocol/mod.rs | 247 +++++++ .../ak-lib/src/tokio/proxy_protocol/utils.rs | 39 ++ .../ak-lib/src/tokio/proxy_protocol/v1.rs | 110 ++++ .../ak-lib/src/tokio/proxy_protocol/v2.rs | 122 ++++ 13 files changed, 1191 insertions(+), 6 deletions(-) create mode 100644 packages/ak-lib/src/tokio/mod.rs create mode 100644 packages/ak-lib/src/tokio/proxy_protocol/LICENSE create mode 100644 packages/ak-lib/src/tokio/proxy_protocol/README.md create mode 100644 packages/ak-lib/src/tokio/proxy_protocol/header.rs create mode 100644 packages/ak-lib/src/tokio/proxy_protocol/mod.rs create mode 100644 packages/ak-lib/src/tokio/proxy_protocol/utils.rs create mode 100644 packages/ak-lib/src/tokio/proxy_protocol/v1.rs create mode 100644 packages/ak-lib/src/tokio/proxy_protocol/v2.rs diff --git a/.cargo/rustfmt.toml b/.cargo/rustfmt.toml index 85ee115daf17..573ed4d1292c 100644 --- a/.cargo/rustfmt.toml +++ b/.cargo/rustfmt.toml @@ -12,5 +12,5 @@ reorder_impl_items = true style_edition = "2024" use_field_init_shorthand = true use_try_shorthand = true -where_single_line = true +where_single_line = false wrap_comments = true diff --git a/Cargo.lock b/Cargo.lock index c86d846a1b20..091a9dbee9c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,13 @@ dependencies = [ [[package]] name = "authentik-lib" version = "2026.5.0-rc1" +dependencies = [ + "eyre", + "pin-project-lite", + "thiserror 2.0.18", + "tokio", + "tracing", +] [[package]] name = "autocfg" @@ -1912,9 +1919,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" diff --git a/Cargo.toml b/Cargo.toml index 2f8dfe6fc787..fef1cf6bd4ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,5 @@ [workspace] -members = [ - "packages/ak-lib", - "packages/client-rust", - "website/scripts/docsmg", -] +members = ["packages/ak-lib", "packages/client-rust", "website/scripts/docsmg"] resolver = "3" [workspace.package] @@ -23,6 +19,7 @@ clap = { version = "= 4.6.0", features = ["derive", "env"] } colored = "= 3.1.1" dotenvy = "= 0.15.7" eyre = "= 0.6.12" +pin-project-lite = "= 0.2.17" regex = "= 1.12.3" reqwest = { version = "= 0.13.2", features = [ "form", @@ -46,6 +43,8 @@ serde_repr = "= 0.1.20" serde_with = { version = "= 3.18.0", default-features = false, features = [ "base64", ] } +thiserror = "= 2.0.18" +tracing = "= 0.1.44" tokio = { version = "= 1.50.0", features = ["full", "tracing"] } tokio-util = { version = "= 0.7.18", features = ["full"] } url = "= 2.5.8" diff --git a/packages/ak-lib/Cargo.toml b/packages/ak-lib/Cargo.toml index a935ca6288c9..f6ab32ab4ee1 100644 --- a/packages/ak-lib/Cargo.toml +++ b/packages/ak-lib/Cargo.toml @@ -10,6 +10,11 @@ license-file.workspace = true publish.workspace = true [dependencies] +eyre.workspace = true +pin-project-lite.workspace = true +tokio.workspace = true +thiserror.workspace = true +tracing.workspace = true [dev-dependencies] diff --git a/packages/ak-lib/src/lib.rs b/packages/ak-lib/src/lib.rs index e63d65161f55..0bdde0a45e11 100644 --- a/packages/ak-lib/src/lib.rs +++ b/packages/ak-lib/src/lib.rs @@ -1,5 +1,7 @@ //! Various utilities used by other crates +pub mod tokio; + pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub fn authentik_build_hash(fallback: Option) -> String { diff --git a/packages/ak-lib/src/tokio/mod.rs b/packages/ak-lib/src/tokio/mod.rs new file mode 100644 index 000000000000..ba0afd914aee --- /dev/null +++ b/packages/ak-lib/src/tokio/mod.rs @@ -0,0 +1,3 @@ +//! [`tokio`] extensions. + +pub mod proxy_protocol; diff --git a/packages/ak-lib/src/tokio/proxy_protocol/LICENSE b/packages/ak-lib/src/tokio/proxy_protocol/LICENSE new file mode 100644 index 000000000000..c2684103f2f8 --- /dev/null +++ b/packages/ak-lib/src/tokio/proxy_protocol/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2026 Authentik Security Inc. +Copyright (c) 2023 Tibor Djurica Potpara + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/ak-lib/src/tokio/proxy_protocol/README.md b/packages/ak-lib/src/tokio/proxy_protocol/README.md new file mode 100644 index 000000000000..fb17e9742385 --- /dev/null +++ b/packages/ak-lib/src/tokio/proxy_protocol/README.md @@ -0,0 +1 @@ +This is a fork of https://github.com/tibordp/proxy-header/, with the sync code removed, the encoding code removed, and the ability to make the PROXY protocol optional. diff --git a/packages/ak-lib/src/tokio/proxy_protocol/header.rs b/packages/ak-lib/src/tokio/proxy_protocol/header.rs new file mode 100644 index 000000000000..f634e7fe6abe --- /dev/null +++ b/packages/ak-lib/src/tokio/proxy_protocol/header.rs @@ -0,0 +1,616 @@ +//! PROXY protocol header definition. + +use std::{borrow::Cow, fmt, net::SocketAddr, str::from_utf8}; + +use thiserror::Error; +use tracing::instrument; + +/// Protocol type +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum Protocol { + /// Stream protocol (TCP) + Stream, + /// Datagram protocol (UDP) + Datagram, +} + +/// Address information from a PROXY protocol header +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct Address { + /// Protocol type + pub protocol: Protocol, + /// Source address (of the actual client) + pub source: SocketAddr, + /// Destination address (of the proxy) + pub destination: SocketAddr, +} + +macro_rules! tlv { + ($self:expr, $kind:ident) => {{ + $self.tlvs().find_map(|f| match f { + Ok(Tlv::$kind(v)) => Some(v), + _ => None, + }) + }}; +} + +macro_rules! tlv_borrowed { + ($self:expr, $kind:ident) => {{ + $self.tlvs().find_map(|f| match f { + Ok(Tlv::$kind(v)) => match v { + // It is more ergonomic to return the borrowed value directly rather + // than it wrapped in a `Cow::Borrowed`. We know that tlvs always borrows + // so we can safely unwrap the `Cow::Borrowed` and return the borrowed value. + Cow::Owned(_) => unreachable!(), + Cow::Borrowed(v) => Some(v), + }, + _ => None, + }) + }}; +} + +/// Iterator over PROXY protocol TLV fields +pub struct Tlvs<'a> { + buf: &'a [u8], +} + +#[expect( + clippy::missing_trait_methods, + reason = "we don't need to implement the other methods here" +)] +impl<'a> Iterator for Tlvs<'a> { + type Item = Result, ProxyProtocolError>; + + fn next(&mut self) -> Option { + if self.buf.is_empty() { + return None; + } + + let kind = self.buf[0]; + match self + .buf + .get(1..3) + .map(|s| -> usize { u16::from_be_bytes(s.try_into().expect("infallible")).into() }) + { + Some(u) if u + 3 <= self.buf.len() => { + let (ret, new) = self.buf.split_at(3 + u); + self.buf = new; + + Some(Tlv::decode(kind, &ret[3..])) + } + _ => { + // Malformed TLV, cannot continue + self.buf = &[]; + Some(Err(ProxyProtocolError::Invalid)) + } + } + } +} + +/// Typed TLV field +/// +/// Represents the currently known types of TLV fields from the PROXY protocol specification. +/// Non-recognized TLV fields are represented as [`Tlv::Custom`]. +#[non_exhaustive] +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Tlv<'a> { + /// Application-Layer Protocol Negotiation (ALPN). It is a byte sequence defining the upper + /// layer protocol in use over the connection. The most common use case will be to pass the + /// exact copy of the ALPN extension of the Transport Layer Security (TLS) protocol as defined + /// by RFC7301. + Alpn(Cow<'a, [u8]>), + + /// Contains the host name value passed by the client, as an UTF-8 encoded string. In case of + /// TLS being used on the client connection, this is the exact copy of the `server_name` + /// extension as defined by RFC3546, section 3.1, often referred to as SNI. There are probably + /// other situations where an authority can be mentionned on a connection without TLS being + /// involved at all. + Authority(Cow<'a, str>), + + /// The value of the type `PP2_TYPE_CRC32C` is a 32-bit number storing the `CRC32c` checksum of + /// the PROXY protocol header. + /// + /// When the checksum is supported by the sender after constructing the header the sender MUST: + /// + /// - initialize the checksum field to '0's. + /// + /// - calculate the `CRC32c` checksum of the PROXY header as described in RFC4960, Appendix B. + /// + /// - put the resultant value into the checksum field, and leave the rest of the bits unchanged. + /// + /// If the checksum is provided as part of the PROXY header and the checksum functionality is + /// supported by the receiver, the receiver MUST: + /// + /// - store the received `CRC32c` checksum value aside. + /// + /// - replace the 32 bits of the checksum field in the received PROXY header with all '0's and + /// calculate a `CRC32c` checksum value of the whole PROXY header. + /// + /// - verify that the calculated `CRC32c` checksum is the same as the received `CRC32c` + /// checksum. If it is not, the receiver MUST treat the TCP connection providing the header as + /// invalid. + /// + /// The default procedure for handling an invalid TCP connection is to abort it. + Crc32c(u32), + + /// The TLV of this type should be ignored when parsed. The value is zero or more bytes. Can be + /// used for data padding or alignment. Note that it can be used to align only by 3 or more + /// bytes because a TLV can not be smaller than that. + Noop(usize), + + /// The value of the type `PP2_TYPE_UNIQUE_ID` is an opaque byte sequence of up to + /// 128 bytes generated by the upstream proxy that uniquely identifies the connection. + /// + /// The unique ID can be used to easily correlate connections across multiple layers of + /// proxies, without needing to look up IP addresses and port numbers. + UniqueId(Cow<'a, [u8]>), + + /// SSL (TLS) information + /// + /// See [`SslInfo`] for more information. + Ssl(SslInfo<'a>), + + /// The type `PP2_TYPE_NETNS` defines the value as the US-ASCII string representation of the + /// namespace's name. + Netns(Cow<'a, str>), + + // The following can only appear as a sub-TLV of SslInfo + /// SSL/TLS version + SslVersion(Cow<'a, str>), + /// In all cases, the string representation (in UTF8) of the Common Name field (OID: 2.5.4.3) + /// of the client certificate's Distinguished Name, is appended using the TLV format and the + /// type `PP2_SUBTYPE_SSL_CN`. E.g. "example.com". + SslCn(Cow<'a, str>), + /// The second level TLV `PP2_SUBTYPE_SSL_CIPHER` provides the US-ASCII string name of the used + /// cipher, for example "ECDHE-RSA-AES128-GCM-SHA256". + SslCipher(Cow<'a, str>), + /// The second level TLV `PP2_SUBTYPE_SSL_SIG_ALG` provides the US-ASCII string name of the + /// algorithm used to sign the certificate presented by the frontend when the incoming + /// connection was made over an SSL/TLS transport layer, for example "SHA256". + SslSigAlg(Cow<'a, str>), + /// The second level TLV `PP2_SUBTYPE_SSL_KEY_ALG` provides the US-ASCII string name of the + /// algorithm used to generate the key of the certificate presented by the frontend when the + /// incoming connection was made over an SSL/TLS transport layer, for example "RSA2048". + SslKeyAlg(Cow<'a, str>), + + /// Unrecognized or custom TLV field + Custom(u8, Cow<'a, [u8]>), +} + +impl<'a> Tlv<'a> { + fn decode(kind: u8, data: &'a [u8]) -> Result { + match kind { + 0x01 => Ok(Self::Alpn(data.into())), + 0x02 => Ok(Self::Authority( + from_utf8(data) + .map_err(|_| ProxyProtocolError::Invalid)? + .into(), + )), + 0x03 => Ok(Self::Crc32c(u32::from_be_bytes( + data.try_into().map_err(|_| ProxyProtocolError::Invalid)?, + ))), + 0x04 => Ok(Self::Noop(data.len())), + 0x05 => Ok(Self::UniqueId(data.into())), + 0x20 => Ok(Tlv::Ssl(SslInfo( + *data.first().ok_or(ProxyProtocolError::Invalid)?, + u32::from_be_bytes( + data.get(1..5) + .ok_or(ProxyProtocolError::Invalid)? + .try_into() + .map_err(|_| ProxyProtocolError::Invalid)?, + ), + data.get(5..).ok_or(ProxyProtocolError::Invalid)?.into(), + ))), + 0x21 => Ok(Self::SslVersion( + from_utf8(data) + .map_err(|_| ProxyProtocolError::Invalid)? + .into(), + )), + 0x22 => Ok(Self::SslCn( + from_utf8(data) + .map_err(|_| ProxyProtocolError::Invalid)? + .into(), + )), + 0x23 => Ok(Self::SslCipher( + from_utf8(data) + .map_err(|_| ProxyProtocolError::Invalid)? + .into(), + )), + 0x24 => Ok(Self::SslSigAlg( + from_utf8(data) + .map_err(|_| ProxyProtocolError::Invalid)? + .into(), + )), + 0x25 => Ok(Self::SslKeyAlg( + from_utf8(data) + .map_err(|_| ProxyProtocolError::Invalid)? + .into(), + )), + 0x30 => Ok(Self::Netns( + from_utf8(data) + .map_err(|_| ProxyProtocolError::Invalid)? + .into(), + )), + t => Ok(Self::Custom(t, data.into())), + } + } + + pub fn into_owned(self) -> Tlv<'static> { + match self { + Self::Alpn(v) => Tlv::Alpn(Cow::Owned(v.into_owned())), + Self::Authority(v) => Tlv::Authority(Cow::Owned(v.into_owned())), + Self::Crc32c(v) => Tlv::Crc32c(v), + Self::Noop(v) => Tlv::Noop(v), + Self::UniqueId(v) => Tlv::UniqueId(Cow::Owned(v.into_owned())), + Self::Ssl(v) => Tlv::Ssl(v.into_owned()), + Self::Netns(v) => Tlv::Netns(Cow::Owned(v.into_owned())), + Self::SslVersion(v) => Tlv::SslVersion(Cow::Owned(v.into_owned())), + Self::SslCn(v) => Tlv::SslCn(Cow::Owned(v.into_owned())), + Self::SslCipher(v) => Tlv::SslCipher(Cow::Owned(v.into_owned())), + Self::SslSigAlg(v) => Tlv::SslSigAlg(Cow::Owned(v.into_owned())), + Self::SslKeyAlg(v) => Tlv::SslKeyAlg(Cow::Owned(v.into_owned())), + Self::Custom(a, v) => Tlv::Custom(a, Cow::Owned(v.into_owned())), + } + } +} + +/// SSL information from a PROXY protocol header +#[derive(PartialEq, Eq, Clone)] +pub struct SslInfo<'a>(u8, u32, Cow<'a, [u8]>); + +impl SslInfo<'_> { + /// Client connected over SSL/TLS + /// + /// The `PP2_CLIENT_SSL` flag indicates that the client connected over SSL/TLS. When this field + /// is present, the US-ASCII string representation of the TLS version is appended at the end of + /// the field in the TLV format using the type `PP2_SUBTYPE_SSL_VERSION`. + pub fn client_ssl(&self) -> bool { + self.0 & 0x01 != 0 + } + + /// Client certificate presented in the connection + /// + /// `PP2_CLIENT_CERT_CONN` indicates that the client provided a certificate over the current + /// connection. + pub fn client_cert_conn(&self) -> bool { + self.0 & 0x02 != 0 + } + + /// Client certificate presented in the session + /// + /// `PP2_CLIENT_CERT_SESS` indicates that the client provided a certificate at least once over + /// the TLS session this connection belongs to. + pub fn client_cert_sess(&self) -> bool { + self.0 & 0x04 != 0 + } + + /// Whether the certificate was verified + /// + /// The verify field will be zero if the client presented a certificate and it was successfully + /// verified, and non-zero otherwise. + pub fn verify(&self) -> u32 { + self.1 + } + + /// Iterator over all TLV (type-length-value) fields + pub fn tlvs(&self) -> Tlvs<'_> { + Tlvs { buf: &self.2 } + } + + // Convenience accessors for common TLVs + + /// SSL version + /// + /// See [`Tlv::SslVersion`] for more information. + pub fn version(&self) -> Option<&str> { + tlv_borrowed!(self, SslVersion) + } + + /// SSL CN + /// + /// See [`Tlv::SslCn`] for more information. + pub fn cn(&self) -> Option<&str> { + tlv_borrowed!(self, SslCn) + } + + /// SSL cipher + /// + /// See [`Tlv::SslCipher`] for more information. + pub fn cipher(&self) -> Option<&str> { + tlv_borrowed!(self, SslCipher) + } + + /// SSL signature algorithm + /// + /// See [`Tlv::SslSigAlg`] for more information. + pub fn sig_alg(&self) -> Option<&str> { + tlv_borrowed!(self, SslSigAlg) + } + + /// SSL key algorithm + /// + /// See [`Tlv::SslKeyAlg`] for more information. + pub fn key_alg(&self) -> Option<&str> { + tlv_borrowed!(self, SslKeyAlg) + } + + /// Returns an owned version of this struct + pub fn into_owned(self) -> SslInfo<'static> { + SslInfo(self.0, self.1, Cow::Owned(self.2.into_owned())) + } +} + +impl fmt::Debug for SslInfo<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SslInfo") + .field("verify", &self.verify()) + .field("client_ssl", &self.client_ssl()) + .field("client_cert_conn", &self.client_cert_conn()) + .field("client_cert_sess", &self.client_cert_sess()) + .field("fields", &self.tlvs().collect::>()) + .finish() + } +} + +/// A PROXY protocol header +#[derive(Default, PartialEq, Eq, Clone, Debug)] +pub struct Header<'a>(pub(super) Option
, pub(super) Cow<'a, [u8]>); + +impl<'a> Header<'a> { + /// Attempt to parse a PROXY protocol header from the given buffer + /// + /// Returns the parsed header and the number of bytes consumed from the buffer. If the header + /// is incomplete, returns [`ProxyProtocolError::BufferTooShort`] so more data can be read from + /// the socket. + /// + /// If the header is malformed or unsupported, returns [`ProxyProtocolError::Invalid`]. + /// + /// This function will borrow the buffer for the lifetime of the returned header. If + /// you need to keep the header around for longer than the buffer, use + /// [`Header::into_owned`]. + #[instrument(skip_all)] + pub fn parse(buf: &'a [u8]) -> Result<(Self, usize), ProxyProtocolError> { + match buf.first() { + Some(b'P') => super::v1::decode(buf), + Some(b'\r') => super::v2::decode(buf), + None => Err(ProxyProtocolError::BufferTooShort), + _ => Err(ProxyProtocolError::Invalid), + } + } + + /// Proxied address information + /// + /// If `None`, this indicates so-called "local" mode, where the connection is not proxied. + /// This is usually the case when the connection is initiated by the proxy itself, e.g. for + /// health checks. + pub fn proxied_address(&self) -> Option<&Address> { + self.0.as_ref() + } + + /// Iterator that yields all extension TLV (type-length-value) fields present in the header + /// + /// See [`Tlv`] for more information on the different types of TLV fields. + pub fn tlvs(&self) -> Tlvs<'_> { + Tlvs { buf: &self.1 } + } + + // Convenience accessors for common fields + + /// Raw ALPN extension data + /// + /// See [`Tlv::Alpn`] for more information. + pub fn alpn(&self) -> Option<&[u8]> { + tlv_borrowed!(self, Alpn) + } + + /// Authority - typically the hostname of the client (SNI) + /// + /// See [`Tlv::Authority`] for more information. + pub fn authority(&self) -> Option<&str> { + tlv_borrowed!(self, Authority) + } + + /// `CRC32c` checksum of the address information + /// + /// See [`Tlv::Crc32c`] for more information. + pub fn crc32c(&self) -> Option { + tlv!(self, Crc32c) + } + + /// Unique ID of the connection + /// + /// See [`Tlv::UniqueId`] for more information. + pub fn unique_id(&self) -> Option<&[u8]> { + tlv_borrowed!(self, UniqueId) + } + + /// SSL information + /// + /// See [`Tlv::Ssl`] for more information. + pub fn ssl(&self) -> Option> { + tlv!(self, Ssl) + } + + /// Network namespace + /// + /// See [`Tlv::Netns`] for more information. + pub fn netns(&self) -> Option<&str> { + tlv_borrowed!(self, Netns) + } + + /// Returns an owned version of this struct + pub fn into_owned(self) -> Header<'static> { + Header(self.0, Cow::Owned(self.1.into_owned())) + } +} + +#[derive(Debug, PartialEq, Eq, Error)] +pub enum ProxyProtocolError { + #[error("The buffer is too short to contain a complete PROXY protocol header")] + BufferTooShort, + #[error("The PROXY protocol header is malformed")] + Invalid, +} + +#[cfg(test)] +mod tests { + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + use super::*; + + const V1_UNKNOWN: &[u8] = b"PROXY UNKNOWN\r\n"; + + const V1_TCPV4: &[u8] = b"PROXY TCP4 127.0.0.1 192.168.0.1 12345 443\r\n"; + const V1_TCPV6: &[u8] = b"PROXY TCP6 2001:db8::1 ::1 12345 443\r\n"; + + const V2_LOCAL: &[u8] = + b"\r\n\r\n\0\r\nQUIT\n \0\0\x0f\x03\0\x04\x88\x9d\xa1\xdf \0\x05\0\0\0\0\0"; + + const V2_TCPV4: &[u8] = &[ + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 17, 0, 12, 127, 0, 0, 1, 192, 168, 0, 1, + 48, 57, 1, 187, + ]; + const V2_TCPV6: &[u8] = &[ + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 33, 0, 36, 32, 1, 13, 184, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 48, 57, 1, 187, + ]; + const V2_TCPV4_TLV: &[u8] = &[ + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 17, 0, 104, 127, 0, 0, 1, 192, 168, 0, + 1, 48, 57, 1, 187, 3, 0, 4, 211, 153, 216, 216, 5, 0, 4, 49, 50, 51, 52, 32, 0, 75, 7, 0, + 0, 0, 0, 33, 0, 7, 84, 76, 83, 118, 49, 46, 51, 34, 0, 9, 108, 111, 99, 97, 108, 104, 111, + 115, 116, 37, 0, 7, 82, 83, 65, 52, 48, 57, 54, 36, 0, 10, 82, 83, 65, 45, 83, 72, 65, 50, + 53, 54, 35, 0, 22, 84, 76, 83, 95, 65, 69, 83, 95, 50, 53, 54, 95, 71, 67, 77, 95, 83, 72, + 65, 51, 56, 52, + ]; + + #[test] + fn parse_proxy_header_too_short() { + for case in &[ + V1_TCPV4, + V1_TCPV6, + V1_UNKNOWN, + V2_TCPV4, + V2_TCPV6, + V2_TCPV4_TLV, + V2_LOCAL, + ] { + for i in 0..case.len() { + assert!(matches!( + Header::parse(&case[..i]), + Err(ProxyProtocolError::BufferTooShort) + )); + } + + Header::parse(case).expect("failed to parse header"); + } + } + + #[test] + fn parse_proxy_header_v1_unterminated() { + let line = b"PROXY TCP4 THISISSTORYALLABOUTHOWMYLIFEGOTFLIPPEDTURNEDUPSIDEDOWNANDIDLIKETOTAKEAMINUTEJUSTSITRIGHTTHEREANDILLTELLYOUHOWIGOTTHEPRINCEOFAIR"; + assert!(matches!( + Header::parse(line), + Err(ProxyProtocolError::Invalid) + )); + } + + #[test] + fn parse_proxy_header_v1() { + let (res, consumed) = Header::parse(V1_TCPV4).expect("failed to parse"); + assert_eq!(consumed, V1_TCPV4.len()); + assert_eq!( + res.0, + Some(Address { + protocol: Protocol::Stream, + source: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 12345), + destination: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)), 443), + }) + ); + assert_eq!(res.1, vec![0; 0]); + + let (res, consumed) = Header::parse(V1_TCPV6).expect("failed to parse"); + assert_eq!(consumed, V1_TCPV6.len()); + assert_eq!( + res.0, + Some(Address { + protocol: Protocol::Stream, + source: SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 12345 + ), + destination: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 443), + }) + ); + assert_eq!(res.1, vec![0; 0]); + } + + #[test] + fn parse_proxy_header_v2() { + let (res, consumed) = Header::parse(V2_LOCAL).expect("failed to parse"); + assert_eq!(consumed, V2_LOCAL.len()); + assert_eq!(res.0, None); + + let (res, consumed) = Header::parse(V2_TCPV4).expect("failed to parse"); + assert_eq!(consumed, V2_TCPV4.len()); + assert_eq!( + res.0, + Some(Address { + protocol: Protocol::Stream, + source: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 12345), + destination: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)), 443), + }) + ); + assert_eq!(res.1, vec![0; 0]); + + let (res, consumed) = Header::parse(V2_TCPV6).expect("failed to parse"); + assert_eq!(consumed, V2_TCPV6.len()); + assert_eq!( + res.0, + Some(Address { + protocol: Protocol::Stream, + source: SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 12345 + ), + destination: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 443), + }) + ); + assert_eq!(res.1, vec![0; 0]); + } + + #[test] + fn parse_proxy_header_v2_with_tlvs() { + let (res, _) = Header::parse(V2_TCPV4_TLV).expect("failed to parse"); + + let mut fields = res.tlvs(); + assert_eq!(fields.next(), Some(Ok(Tlv::Crc32c(0xd399_d8d8)))); + assert_eq!(fields.next(), Some(Ok(Tlv::UniqueId(b"1234"[..].into())))); + + let ssl = fields + .next() + .expect("next tlv missing") + .expect("tlv parsing failed"); + let Tlv::Ssl(ssl) = ssl else { + panic!("expected SSL TLV"); + }; + + assert_eq!(ssl.verify(), 0); + assert!(ssl.client_ssl()); + assert!(ssl.client_cert_conn()); + assert!(ssl.client_cert_sess()); + + let mut f = ssl.tlvs(); + + assert_eq!(f.next(), Some(Ok(Tlv::SslVersion("TLSv1.3".into())))); + assert_eq!(f.next(), Some(Ok(Tlv::SslCn("localhost".into())))); + assert_eq!(f.next(), Some(Ok(Tlv::SslKeyAlg("RSA4096".into())))); + assert_eq!(f.next(), Some(Ok(Tlv::SslSigAlg("RSA-SHA256".into())))); + assert_eq!( + f.next(), + Some(Ok(Tlv::SslCipher("TLS_AES_256_GCM_SHA384".into()))) + ); + assert!(f.next().is_none()); + + assert!(fields.next().is_none()); + } +} diff --git a/packages/ak-lib/src/tokio/proxy_protocol/mod.rs b/packages/ak-lib/src/tokio/proxy_protocol/mod.rs new file mode 100644 index 000000000000..f7aa3f54fa7a --- /dev/null +++ b/packages/ak-lib/src/tokio/proxy_protocol/mod.rs @@ -0,0 +1,247 @@ +use std::{ + cmp::min, + io, + io::IoSlice, + ops::Deref, + pin::Pin, + task::{Context, Poll}, +}; + +use eyre::{Result, eyre}; +use pin_project_lite::pin_project; +use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt as _, AsyncWrite, ReadBuf}; +use tracing::instrument; + +use crate::tokio::proxy_protocol::header::{Header, ProxyProtocolError}; + +pub mod header; +mod utils; +mod v1; +mod v2; + +// Length of the read buffer +const READ_BUFFER_LEN: usize = 536; + +pin_project! { + pub struct ProxyProtocolStream { + #[pin] + inner: S, + remaining: Vec, + header: Option>, + } +} + +impl ProxyProtocolStream { + pub fn header(&self) -> Option<&Header<'static>> { + self.header.as_ref() + } + + fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut S> { + self.project().inner + } + + pub fn try_into_stream(self) -> Result { + if self.remaining.is_empty() { + Ok(self.inner) + } else { + Err(eyre!( + "Cannot return inner stream because buffer is not empty" + )) + } + } +} + +impl AsRef for ProxyProtocolStream { + fn as_ref(&self) -> &S { + &self.inner + } +} + +impl Deref for ProxyProtocolStream { + type Target = S; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl ProxyProtocolStream +where + S: AsyncRead + Unpin, +{ + #[instrument(skip_all)] + pub async fn new(mut inner: S) -> Result { + let mut remaining = Vec::with_capacity(READ_BUFFER_LEN); + + loop { + let bytes_read = inner.read_buf(&mut remaining).await?; + if bytes_read == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "end of stream", + )); + } + + match Header::parse(&remaining) { + Ok((header, consumed)) => { + let header = header.into_owned(); + remaining.drain(..consumed); + + return Ok(Self { + inner, + remaining, + header: Some(header), + }); + } + Err(ProxyProtocolError::BufferTooShort) => {} + // Something went wrong parsing the PROXY protocol. We assume that we weren't meant + // to parse it, and that this is just a regular stream without the PROXY protocol. + Err(_) => { + return Ok(Self { + inner, + remaining, + header: None, + }); + } + } + } + } +} + +impl AsyncRead for ProxyProtocolStream +where + S: AsyncRead, +{ + #[instrument(skip_all)] + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + + if !this.remaining.is_empty() { + let to_copy = min(this.remaining.len(), buf.remaining()); + + buf.put_slice(&this.remaining[..to_copy]); + this.remaining.drain(..to_copy); + + return Poll::Ready(Ok(())); + } + + this.inner.poll_read(cx, buf) + } +} + +impl AsyncBufRead for ProxyProtocolStream +where + S: AsyncBufRead, +{ + #[instrument(skip_all)] + fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + + if !this.remaining.is_empty() { + return Poll::Ready(Ok(&this.remaining[..])); + } + + this.inner.poll_fill_buf(cx) + } + + #[instrument(skip_all)] + fn consume(self: Pin<&mut Self>, amt: usize) { + let this = self.project(); + + if this.remaining.is_empty() { + this.inner.consume(amt); + } else { + let len = this.remaining.len(); + if amt <= len { + this.remaining.drain(..amt); + } else { + this.remaining.drain(..len); + this.inner.consume(amt - len); + } + } + } +} + +impl AsyncWrite for ProxyProtocolStream +where + S: AsyncWrite, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.get_pin_mut().poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.get_pin_mut().poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.get_pin_mut().poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll> { + self.get_pin_mut().poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.inner.is_write_vectored() + } +} + +#[cfg(test)] +mod tests { + use std::{ + io::Cursor, + net::{IpAddr, Ipv4Addr, SocketAddr}, + }; + + use super::{ + header::{Address, Protocol}, + *, + }; + + #[tokio::test] + async fn parse() { + let mut buf = [0; 1024]; + let header = b"PROXY TCP4 127.0.0.1 192.168.0.1 12345 443\r\n"; + buf[..header.len()].copy_from_slice(header); + buf[header.len()..].fill(255); + + let mut stream = Cursor::new(&buf); + + let mut proxied = ProxyProtocolStream::new(&mut stream) + .await + .expect("failed to create stream"); + assert_eq!( + proxied.header(), + Some(Header( + Some(Address { + protocol: Protocol::Stream, + source: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 12345), + destination: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)), 443), + }), + vec![0; 0].into(), + )) + .as_ref() + ); + + let mut buf = Vec::new(); + proxied + .read_to_end(&mut buf) + .await + .expect("failed to read from stream"); + assert_eq!(buf.len(), 1024 - header.len()); + assert!(buf.into_iter().all(|b| b == 255)); + } +} diff --git a/packages/ak-lib/src/tokio/proxy_protocol/utils.rs b/packages/ak-lib/src/tokio/proxy_protocol/utils.rs new file mode 100644 index 000000000000..0a7935b69a62 --- /dev/null +++ b/packages/ak-lib/src/tokio/proxy_protocol/utils.rs @@ -0,0 +1,39 @@ +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +pub(super) fn read_until(buf: &[u8], delim: u8) -> Option<&[u8]> { + for i in 0..buf.len() { + if buf[i] == delim { + return Some(&buf[..i]); + } + } + None +} + +pub(super) trait AddressFamily: FromStr + Into { + const BYTES: usize; + + fn from_slice(slice: &[u8]) -> Self; +} + +impl AddressFamily for Ipv4Addr { + #[expect(clippy::as_conversions, reason = "will always be in bounds")] + const BYTES: usize = (Self::BITS / 8) as usize; + + fn from_slice(slice: &[u8]) -> Self { + let arr: [u8; Self::BYTES] = slice.try_into().expect("slice must be 4 bytes"); + arr.into() + } +} + +impl AddressFamily for Ipv6Addr { + #[expect(clippy::as_conversions, reason = "will always be in bounds")] + const BYTES: usize = (Self::BITS / 8) as usize; + + fn from_slice(slice: &[u8]) -> Self { + let arr: [u8; Self::BYTES] = slice.try_into().expect("slice must be 16 bytes"); + arr.into() + } +} diff --git a/packages/ak-lib/src/tokio/proxy_protocol/v1.rs b/packages/ak-lib/src/tokio/proxy_protocol/v1.rs new file mode 100644 index 000000000000..d87fb434a42f --- /dev/null +++ b/packages/ak-lib/src/tokio/proxy_protocol/v1.rs @@ -0,0 +1,110 @@ +use std::{ + borrow::Cow, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + str::{FromStr as _, from_utf8}, +}; + +use super::{ + header::{Address, Header, Protocol, ProxyProtocolError}, + utils::{AddressFamily, read_until}, +}; + +const MAX_LENGTH: usize = 107; +const GREETING: &[u8] = b"PROXY"; +const UNKNOWN: &[u8] = b"PROXY UNKNOWN\r\n"; +// All other valid PROXY headers are longer than this +const MIN_LENGTH: usize = UNKNOWN.len(); + +fn parse_addr(buf: &[u8], pos: &mut usize) -> Result { + let Some(address) = read_until(&buf[*pos..], b' ') else { + return Err(ProxyProtocolError::BufferTooShort); + }; + + let addr = from_utf8(address) + .map_err(|_| ProxyProtocolError::Invalid) + .and_then(|s| A::from_str(s).map_err(|_| ProxyProtocolError::Invalid))?; + *pos += address.len() + 1; + + Ok(addr) +} + +fn parse_port(buf: &[u8], pos: &mut usize, delim: u8) -> Result { + let Some(port) = read_until(&buf[*pos..], delim) else { + return Err(ProxyProtocolError::BufferTooShort); + }; + + let p = from_utf8(port) + .map_err(|_| ProxyProtocolError::Invalid) + .and_then(|s| u16::from_str(s).map_err(|_| ProxyProtocolError::Invalid))?; + *pos += port.len() + 1; + + Ok(p) +} + +fn parse_addrs( + buf: &[u8], + pos: &mut usize, +) -> Result { + let src_addr: A = parse_addr(buf, pos)?; + let dst_addr: A = parse_addr(buf, pos)?; + let src_port = parse_port(buf, pos, b' ')?; + let dst_port = parse_port(buf, pos, b'\r')?; + + Ok(Address { + protocol: Protocol::Stream, // v1 only supports TCP + source: SocketAddr::new(src_addr.into(), src_port), + destination: SocketAddr::new(dst_addr.into(), dst_port), + }) +} + +fn decode_inner(buf: &[u8]) -> Result<(Header<'_>, usize), ProxyProtocolError> { + let mut pos = 0; + + if buf.len() < MIN_LENGTH { + return Err(ProxyProtocolError::BufferTooShort); + } + if !buf.starts_with(GREETING) { + return Err(ProxyProtocolError::Invalid); + } + pos += GREETING.len() + 1; + + let addrs = if buf[pos..].starts_with(b"UNKNOWN") { + let Some(rest) = read_until(&buf[pos..], b'\r') else { + return Err(ProxyProtocolError::BufferTooShort); + }; + pos += rest.len() + 1; + + None + } else { + let proto = &buf[pos..pos + 5]; + pos += 5; + + match proto { + b"TCP4 " => Some(parse_addrs::(buf, &mut pos)?), + b"TCP6 " => Some(parse_addrs::(buf, &mut pos)?), + _ => return Err(ProxyProtocolError::Invalid), + } + }; + + match buf.get(pos) { + Some(b'\n') => pos += 1, + None => return Err(ProxyProtocolError::BufferTooShort), + _ => return Err(ProxyProtocolError::Invalid), + } + + Ok((Header(addrs, Cow::default()), pos)) +} + +/// Decode a version 1 PROXY header from a buffer. +/// +/// Returns the decoded header and the number of bytes consumed from the buffer. +pub(super) fn decode(buf: &[u8]) -> Result<(Header<'_>, usize), ProxyProtocolError> { + // Guard against a malicious client sending a very long header, since it is a + // delimited protocol. + match decode_inner(buf) { + Err(ProxyProtocolError::BufferTooShort) if buf.len() >= MAX_LENGTH => { + Err(ProxyProtocolError::Invalid) + } + other => other, + } +} diff --git a/packages/ak-lib/src/tokio/proxy_protocol/v2.rs b/packages/ak-lib/src/tokio/proxy_protocol/v2.rs new file mode 100644 index 000000000000..7f3c16d0d9ef --- /dev/null +++ b/packages/ak-lib/src/tokio/proxy_protocol/v2.rs @@ -0,0 +1,122 @@ +use std::{ + borrow::Cow, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, +}; + +use super::{ + header::{Address, Header, Protocol, ProxyProtocolError}, + utils::AddressFamily, +}; + +const GREETING: &[u8] = b"\r\n\r\n\x00\r\nQUIT\n"; +const MIN_LENGTH: usize = GREETING.len() + 4; +const AF_UNIX_ADDRS_LEN: usize = 216; + +fn parse_addrs( + buf: &[u8], + pos: &mut usize, + rest: &mut usize, + protocol: Protocol, +) -> Result { + if buf.len() < *pos + T::BYTES * 2 + 4 { + return Err(ProxyProtocolError::BufferTooShort); + } + if *rest < T::BYTES * 2 + 4 { + return Err(ProxyProtocolError::Invalid); + } + + let addr = Address { + protocol, + source: SocketAddr::new( + T::from_slice(&buf[*pos..*pos + T::BYTES]).into(), + u16::from_be_bytes([buf[*pos + T::BYTES * 2], buf[*pos + T::BYTES * 2 + 1]]), + ), + destination: SocketAddr::new( + T::from_slice(&buf[*pos + T::BYTES..*pos + T::BYTES * 2]).into(), + u16::from_be_bytes([buf[*pos + T::BYTES * 2 + 2], buf[*pos + T::BYTES * 2 + 3]]), + ), + }; + + *rest -= T::BYTES * 2 + 4; + *pos += T::BYTES * 2 + 4; + + Ok(addr) +} + +/// Decode a version 2 PROXY header from a buffer. +/// +/// Returns the decoded header and the number of bytes consumed from the buffer. +pub(super) fn decode(buf: &[u8]) -> Result<(Header<'_>, usize), ProxyProtocolError> { + let mut pos = 0; + + if buf.len() < MIN_LENGTH { + return Err(ProxyProtocolError::BufferTooShort); + } + if !buf.starts_with(GREETING) { + return Err(ProxyProtocolError::Invalid); + } + pos += GREETING.len(); + + let is_local = match buf[pos] { + 0x20 => true, + 0x21 => false, + _ => return Err(ProxyProtocolError::Invalid), + }; + let protocol = buf[pos + 1]; + let mut rest: usize = u16::from_be_bytes([buf[pos + 2], buf[pos + 3]]).into(); + pos += 4; + + if buf.len() < pos + rest { + return Err(ProxyProtocolError::BufferTooShort); + } + + let addr_info = match protocol { + 0x00 => None, + 0x11 => Some(parse_addrs::( + buf, + &mut pos, + &mut rest, + Protocol::Stream, + )?), + 0x12 => Some(parse_addrs::( + buf, + &mut pos, + &mut rest, + Protocol::Datagram, + )?), + 0x21 => Some(parse_addrs::( + buf, + &mut pos, + &mut rest, + Protocol::Stream, + )?), + 0x22 => Some(parse_addrs::( + buf, + &mut pos, + &mut rest, + Protocol::Datagram, + )?), + 0x31 | 0x32 => { + // AF_UNIX - we don't parse it, but don't reject it either in case we need the TLVs + if rest < AF_UNIX_ADDRS_LEN { + return Err(ProxyProtocolError::Invalid); + } + rest -= AF_UNIX_ADDRS_LEN; + pos += AF_UNIX_ADDRS_LEN; + None + } + _ => return Err(ProxyProtocolError::Invalid), + }; + + let tlv_data = Cow::Borrowed(&buf[pos..pos + rest]); + + pos += rest; + + let header = if is_local { + Header(None, tlv_data) + } else { + Header(addr_info, tlv_data) + }; + + Ok((header, pos)) +} From a3fea5d6ab970adfb3e4de5f0160c2488c4693fb Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 16:32:16 +0200 Subject: [PATCH 15/36] root: fix rustfmt config Signed-off-by: Marc 'risson' Schmitt --- .cargo/rustfmt.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/.cargo/rustfmt.toml b/.cargo/rustfmt.toml index 85ee115daf17..48e433df8894 100644 --- a/.cargo/rustfmt.toml +++ b/.cargo/rustfmt.toml @@ -12,5 +12,4 @@ reorder_impl_items = true style_edition = "2024" use_field_init_shorthand = true use_try_shorthand = true -where_single_line = true wrap_comments = true From ab239bf9635cf762b8de15253fba1b6d46aae8cd Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 16:39:44 +0200 Subject: [PATCH 16/36] fix import Signed-off-by: Marc 'risson' Schmitt --- packages/ak-lib/src/tokio/proxy_protocol/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ak-lib/src/tokio/proxy_protocol/mod.rs b/packages/ak-lib/src/tokio/proxy_protocol/mod.rs index f7aa3f54fa7a..1a0aebe51ecb 100644 --- a/packages/ak-lib/src/tokio/proxy_protocol/mod.rs +++ b/packages/ak-lib/src/tokio/proxy_protocol/mod.rs @@ -12,7 +12,7 @@ use pin_project_lite::pin_project; use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt as _, AsyncWrite, ReadBuf}; use tracing::instrument; -use crate::tokio::proxy_protocol::header::{Header, ProxyProtocolError}; +use self::header::{Header, ProxyProtocolError}; pub mod header; mod utils; From 75622e9b485e017ec5582be3026482aa1114b1b2 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 16:57:45 +0200 Subject: [PATCH 17/36] packages/ak-axum: init Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 4 ++++ Cargo.toml | 1 + packages/ak-axum/Cargo.toml | 15 +++++++++++++++ packages/ak-axum/src/lib.rs | 1 + 4 files changed, 21 insertions(+) create mode 100644 packages/ak-axum/Cargo.toml create mode 100644 packages/ak-axum/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c86d846a1b20..76f4cf7df65c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,10 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "authentik-axum" +version = "2026.5.0-rc1" + [[package]] name = "authentik-client" version = "2026.5.0-rc1" diff --git a/Cargo.toml b/Cargo.toml index 2f8dfe6fc787..fda39aa954a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "packages/ak-axum", "packages/ak-lib", "packages/client-rust", "website/scripts/docsmg", diff --git a/packages/ak-axum/Cargo.toml b/packages/ak-axum/Cargo.toml new file mode 100644 index 000000000000..a52aba1bcc93 --- /dev/null +++ b/packages/ak-axum/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "authentik-axum" +version.workspace = true +authors.workspace = true +edition.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +publish.workspace = true + +[dependencies] + +[lints] +workspace = true diff --git a/packages/ak-axum/src/lib.rs b/packages/ak-axum/src/lib.rs new file mode 100644 index 000000000000..302c5cdcaa76 --- /dev/null +++ b/packages/ak-axum/src/lib.rs @@ -0,0 +1 @@ +//! Utilities for working with [`axum`]. From c08e2a34ee6c6b60ddbcbaf9c4cf77c0b40d7d5a Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 17:05:01 +0200 Subject: [PATCH 18/36] wip Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 222 +++++++++++++++++++++++++++++++++ Cargo.toml | 4 + packages/ak-axum/Cargo.toml | 5 + packages/ak-axum/src/lib.rs | 2 + packages/ak-axum/src/router.rs | 22 ++++ 5 files changed, 255 insertions(+) create mode 100644 packages/ak-axum/src/router.rs diff --git a/Cargo.lock b/Cargo.lock index 76f4cf7df65c..d9ed9b6378ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,6 +87,13 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "authentik-axum" version = "2026.5.0-rc1" +dependencies = [ + "authentik-lib", + "axum", + "durstr", + "tower", + "tower-http", +] [[package]] name = "authentik-client" @@ -154,6 +161,73 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "axum-macros", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "base64" version = "0.22.1" @@ -186,6 +260,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -358,6 +441,31 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "deranged" version = "0.5.8" @@ -367,6 +475,16 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -401,6 +519,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "durstr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f06c73c39c1530f8356f1da74713d41cfada2c59a042cfcb675ba33de396da" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "either" version = "1.15.0" @@ -535,6 +662,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -666,6 +803,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -680,6 +823,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1005,6 +1149,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" @@ -1579,6 +1729,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -1616,6 +1777,17 @@ dependencies = [ "time", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1847,6 +2019,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -1877,6 +2061,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1892,6 +2077,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", + "tokio", "tower", "tower-layer", "tower-service", @@ -1915,6 +2101,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] @@ -1934,6 +2121,29 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicase" version = "2.9.0" @@ -1976,6 +2186,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2000,6 +2216,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index fda39aa954a4..1328e6d6a2ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,11 @@ publish = false [workspace.dependencies] aws-lc-rs = { version = "= 1.16.2", features = ["fips"] } +axum = { version = "= 0.8.8", features = ["http2", "macros", "ws"] } clap = { version = "= 4.6.0", features = ["derive", "env"] } colored = "= 3.1.1" dotenvy = "= 0.15.7" +durstr = "= 0.5.1" eyre = "= 0.6.12" regex = "= 1.12.3" reqwest = { version = "= 0.13.2", features = [ @@ -49,6 +51,8 @@ serde_with = { version = "= 3.18.0", default-features = false, features = [ ] } tokio = { version = "= 1.50.0", features = ["full", "tracing"] } tokio-util = { version = "= 0.7.18", features = ["full"] } +tower = "= 0.5.3" +tower-http = { version = "= 0.6.8", features = ["timeout"] } url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } diff --git a/packages/ak-axum/Cargo.toml b/packages/ak-axum/Cargo.toml index a52aba1bcc93..e8ee759f1188 100644 --- a/packages/ak-axum/Cargo.toml +++ b/packages/ak-axum/Cargo.toml @@ -10,6 +10,11 @@ license-file.workspace = true publish.workspace = true [dependencies] +axum.workspace = true +durstr.workspace = true +lib.workspace = true +tower.workspace = true +tower-http.workspace = true [lints] workspace = true diff --git a/packages/ak-axum/src/lib.rs b/packages/ak-axum/src/lib.rs index 302c5cdcaa76..9b208fdf90d4 100644 --- a/packages/ak-axum/src/lib.rs +++ b/packages/ak-axum/src/lib.rs @@ -1 +1,3 @@ //! Utilities for working with [`axum`]. + +pub mod router; diff --git a/packages/ak-axum/src/router.rs b/packages/ak-axum/src/router.rs new file mode 100644 index 000000000000..387bf565b11d --- /dev/null +++ b/packages/ak-axum/src/router.rs @@ -0,0 +1,22 @@ +//! Utilities for working with [`Router`]. + +use axum::{Router, http::StatusCode}; +use lib::config; +use tower::ServiceBuilder; +use tower_http::timeout::TimeoutLayer; + +/// Wrap a [`Router`] with common middlewares. +#[inline] +pub fn wrap_router(router: Router) -> Router { + let config = config::get(); + let timeout = durstr::parse(&config.web.timeout_http_read_header) + .expect("Invalid duration in http timeout") + + durstr::parse(&config.web.timeout_http_read).expect("Invalid duration in http timeout") + + durstr::parse(&config.web.timeout_http_write).expect("Invalid duration in http timeout") + + durstr::parse(&config.web.timeout_http_idle).expect("Invalid duration in http timeout"); + let service_builder = ServiceBuilder::new().layer(TimeoutLayer::with_status_code( + StatusCode::REQUEST_TIMEOUT, + timeout, + )); + router.layer(service_builder) +} From 007998460667a616c67612a55a05841988af9de9 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 17:08:07 +0200 Subject: [PATCH 19/36] packages/ak-common: rename from ak-lib Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 2 +- Cargo.toml | 6 ++---- packages/{ak-lib => ak-common}/Cargo.toml | 2 +- packages/{ak-lib => ak-common}/src/lib.rs | 0 4 files changed, 4 insertions(+), 6 deletions(-) rename packages/{ak-lib => ak-common}/Cargo.toml (91%) rename packages/{ak-lib => ak-common}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index c86d846a1b20..53dffa237193 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ ] [[package]] -name = "authentik-lib" +name = "authentik-common" version = "2026.5.0-rc1" [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2f8dfe6fc787..1b6eed7ccbe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ - "packages/ak-lib", + "packages/ak-common", "packages/client-rust", "website/scripts/docsmg", ] @@ -51,9 +51,7 @@ tokio-util = { version = "= 0.7.18", features = ["full"] } url = "= 2.5.8" uuid = { version = "= 1.23.0", features = ["serde", "v4"] } -lib = { package = "authentik-lib", version = "2026.5.0-rc1", path = "./packages/ak-lib" } - -authentik-client = { version = "2026.5.0-rc1", path = "./packages/client-rust" } +ak-common = { package = "authentik-common", version = "2026.5.0-rc1", path = "./packages/ak-common" } [profile.dev.package.backtrace] opt-level = 3 diff --git a/packages/ak-lib/Cargo.toml b/packages/ak-common/Cargo.toml similarity index 91% rename from packages/ak-lib/Cargo.toml rename to packages/ak-common/Cargo.toml index a935ca6288c9..a7b1ed133b30 100644 --- a/packages/ak-lib/Cargo.toml +++ b/packages/ak-common/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "authentik-lib" +name = "authentik-common" version.workspace = true authors.workspace = true edition.workspace = true diff --git a/packages/ak-lib/src/lib.rs b/packages/ak-common/src/lib.rs similarity index 100% rename from packages/ak-lib/src/lib.rs rename to packages/ak-common/src/lib.rs From 095d38cc7f5593232bbb0e324b726f86ad291832 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 17:11:52 +0200 Subject: [PATCH 20/36] fixup Signed-off-by: Marc 'risson' Schmitt --- packages/{ak-lib => ak-common}/src/config/mod.rs | 0 packages/{ak-lib => ak-common}/src/config/schema.rs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/{ak-lib => ak-common}/src/config/mod.rs (100%) rename packages/{ak-lib => ak-common}/src/config/schema.rs (100%) diff --git a/packages/ak-lib/src/config/mod.rs b/packages/ak-common/src/config/mod.rs similarity index 100% rename from packages/ak-lib/src/config/mod.rs rename to packages/ak-common/src/config/mod.rs diff --git a/packages/ak-lib/src/config/schema.rs b/packages/ak-common/src/config/schema.rs similarity index 100% rename from packages/ak-lib/src/config/schema.rs rename to packages/ak-common/src/config/schema.rs From 5c937d7095592e604ca02f5436881bd893e795b2 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 17:40:46 +0200 Subject: [PATCH 21/36] packages/ak-axum/tracing: init Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 2 ++ packages/ak-axum/Cargo.toml | 2 ++ packages/ak-axum/src/lib.rs | 1 + packages/ak-axum/src/router.rs | 24 +++++++++++----- packages/ak-axum/src/tracing.rs | 51 +++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 packages/ak-axum/src/tracing.rs diff --git a/Cargo.lock b/Cargo.lock index b4f90bba2f9f..9b4d8522e947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,8 +106,10 @@ dependencies = [ "authentik-common", "axum", "durstr", + "tokio", "tower", "tower-http", + "tracing", ] [[package]] diff --git a/packages/ak-axum/Cargo.toml b/packages/ak-axum/Cargo.toml index d7054c5031c9..131dadd1032a 100644 --- a/packages/ak-axum/Cargo.toml +++ b/packages/ak-axum/Cargo.toml @@ -13,8 +13,10 @@ publish.workspace = true axum.workspace = true durstr.workspace = true ak-common.workspace = true +tokio.workspace = true tower.workspace = true tower-http.workspace = true +tracing.workspace = true [lints] workspace = true diff --git a/packages/ak-axum/src/lib.rs b/packages/ak-axum/src/lib.rs index 9b208fdf90d4..0d5b125ebcae 100644 --- a/packages/ak-axum/src/lib.rs +++ b/packages/ak-axum/src/lib.rs @@ -1,3 +1,4 @@ //! Utilities for working with [`axum`]. pub mod router; +pub mod tracing; diff --git a/packages/ak-axum/src/router.rs b/packages/ak-axum/src/router.rs index 7ad8a9267e5e..a2722bd744bc 100644 --- a/packages/ak-axum/src/router.rs +++ b/packages/ak-axum/src/router.rs @@ -1,22 +1,32 @@ //! Utilities for working with [`Router`]. use ak_common::config; -use axum::{Router, http::StatusCode}; +use axum::{Router, http::StatusCode, middleware::from_fn}; use tower::ServiceBuilder; use tower_http::timeout::TimeoutLayer; +use crate::tracing::{span_middleware, tracing_middleware}; + /// Wrap a [`Router`] with common middlewares. +/// +/// Set `with_tracing` to [`true`] to log requests. #[inline] -pub fn wrap_router(router: Router) -> Router { +pub fn wrap_router(router: Router, with_tracing: bool) -> Router { let config = config::get(); let timeout = durstr::parse(&config.web.timeout_http_read_header) .expect("Invalid duration in http timeout") + durstr::parse(&config.web.timeout_http_read).expect("Invalid duration in http timeout") + durstr::parse(&config.web.timeout_http_write).expect("Invalid duration in http timeout") + durstr::parse(&config.web.timeout_http_idle).expect("Invalid duration in http timeout"); - let service_builder = ServiceBuilder::new().layer(TimeoutLayer::with_status_code( - StatusCode::REQUEST_TIMEOUT, - timeout, - )); - router.layer(service_builder) + let service_builder = ServiceBuilder::new() + .layer(TimeoutLayer::with_status_code( + StatusCode::REQUEST_TIMEOUT, + timeout, + )) + .layer(from_fn(span_middleware)); + if with_tracing { + router.layer(service_builder.layer(from_fn(tracing_middleware))) + } else { + router.layer(service_builder) + } } diff --git a/packages/ak-axum/src/tracing.rs b/packages/ak-axum/src/tracing.rs new file mode 100644 index 000000000000..c69678aa31b4 --- /dev/null +++ b/packages/ak-axum/src/tracing.rs @@ -0,0 +1,51 @@ +//! Middlewares for tracing requests. + +use std::collections::HashMap; + +use ak_common::config; +use axum::{extract::Request, middleware::Next, response::Response}; +use tokio::time::Instant; +use tracing::{Instrument as _, field, info, info_span, trace}; + +/// Create a [`tracing::Span`] for requests. +pub(crate) async fn span_middleware(request: Request, next: Next) -> Response { + let config = config::get(); + let http_headers = request + .headers() + .iter() + .filter(|(name, _)| { + for header in &config.log.http_headers { + if header.eq_ignore_ascii_case(name.as_str()) { + return true; + } + } + false + }) + .map(|(name, value)| (name.to_string().to_lowercase().replace('-', "_"), value)) + .collect::>(); + let span = info_span!( + "request", + path = %request.uri(), + method = %request.method(), + remote = field::Empty, + scheme = field::Empty, + host = field::Empty, + http_headers = ?http_headers, + ); + next.run(request).instrument(span).await +} + +/// Log requests. +pub(crate) async fn tracing_middleware(request: Request, next: Next) -> Response { + let event = request.uri().clone(); + trace!("request start"); + + let start = Instant::now(); + let response = next.run(request).await; + let runtime = start.elapsed(); + let status = response.status().as_u16(); + + info!(status = status, runtime = runtime.as_millis(), "{event}"); + + response +} From 116e6016a7fb995e2117a0989f7dd4a1027a1ec6 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 18:28:43 +0200 Subject: [PATCH 22/36] packages/ak-axum/server: init Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 3 + packages/ak-axum/Cargo.toml | 3 + packages/ak-axum/src/lib.rs | 1 + packages/ak-axum/src/server.rs | 147 +++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 packages/ak-axum/src/server.rs diff --git a/Cargo.lock b/Cargo.lock index b4f90bba2f9f..bebb15f70087 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,9 +105,12 @@ version = "2026.5.0-rc1" dependencies = [ "authentik-common", "axum", + "axum-server", "durstr", + "eyre", "tower", "tower-http", + "tracing", ] [[package]] diff --git a/packages/ak-axum/Cargo.toml b/packages/ak-axum/Cargo.toml index d7054c5031c9..243a924ee492 100644 --- a/packages/ak-axum/Cargo.toml +++ b/packages/ak-axum/Cargo.toml @@ -11,10 +11,13 @@ publish.workspace = true [dependencies] axum.workspace = true +axum-server.workspace = true durstr.workspace = true ak-common.workspace = true +eyre.workspace = true tower.workspace = true tower-http.workspace = true +tracing.workspace = true [lints] workspace = true diff --git a/packages/ak-axum/src/lib.rs b/packages/ak-axum/src/lib.rs index 9b208fdf90d4..836717255653 100644 --- a/packages/ak-axum/src/lib.rs +++ b/packages/ak-axum/src/lib.rs @@ -1,3 +1,4 @@ //! Utilities for working with [`axum`]. pub mod router; +pub mod server; diff --git a/packages/ak-axum/src/server.rs b/packages/ak-axum/src/server.rs new file mode 100644 index 000000000000..3869fe2525f0 --- /dev/null +++ b/packages/ak-axum/src/server.rs @@ -0,0 +1,147 @@ +//! Utilities to run an axum server. + +use std::{net, os::unix}; + +use ak_common::arbiter::{Arbiter, Tasks}; +use axum::Router; +use axum_server::{ + Handle, + accept::DefaultAcceptor, + tls_rustls::{RustlsAcceptor, RustlsConfig}, +}; +use eyre::Result; +use tracing::info; + +async fn run_plain( + arbiter: Arbiter, + name: &str, + router: Router, + addr: net::SocketAddr, + allow_failure: bool, +) -> Result<()> { + info!(addr = addr.to_string(), "starting {name} server"); + + let handle = Handle::new(); + arbiter.add_net_handle(handle.clone()).await; + + let res = axum_server::Server::bind(addr) + .acceptor(DefaultAcceptor::new()) + .handle(handle) + .serve(router.into_make_service_with_connect_info::()) + .await; + if res.is_err() && allow_failure { + arbiter.shutdown().await; + return Ok(()); + } + res?; + + Ok(()) +} + +/// Start a plaintext server. +/// +/// `name` is only used for observability purposes and should describe which module is starting the +/// server. +/// +/// `allow_failure` allows the server to fail silently. +pub fn start_plain( + tasks: &mut Tasks, + name: &'static str, + router: Router, + addr: net::SocketAddr, + allow_failure: bool, +) -> Result<()> { + let arbiter = tasks.arbiter(); + tasks + .build_task() + .name(&format!("{}::run_plain({name}, {addr})", module_path!())) + .spawn(run_plain(arbiter, name, router, addr, allow_failure))?; + Ok(()) +} + +pub(crate) async fn run_unix( + arbiter: Arbiter, + name: &str, + router: Router, + addr: unix::net::SocketAddr, + allow_failure: bool, +) -> Result<()> { + info!(addr = ?addr, "starting {name} server"); + + let handle = Handle::new(); + arbiter.add_unix_handle(handle.clone()).await; + + let res = axum_server::Server::bind(addr) + .acceptor(DefaultAcceptor::new()) + .handle(handle) + .serve(router.into_make_service()) + .await; + if res.is_err() && allow_failure { + arbiter.shutdown().await; + return Ok(()); + } + res?; + + Ok(()) +} + +/// Start a Unix socket server. +/// +/// `name` is only used for observability purposes and should describe which module is starting the +/// server. +/// +/// `allow_failure` allows the server to fail silently. +pub fn start_unix( + tasks: &mut Tasks, + name: &'static str, + router: Router, + addr: unix::net::SocketAddr, + allow_failure: bool, +) -> Result<()> { + let arbiter = tasks.arbiter(); + tasks + .build_task() + .name(&format!("{}::run_unix({name}, {addr:?})", module_path!())) + .spawn(run_unix(arbiter, name, router, addr, allow_failure))?; + Ok(()) +} + +async fn run_tls( + arbiter: Arbiter, + name: &str, + router: Router, + addr: net::SocketAddr, + config: RustlsConfig, +) -> Result<()> { + info!(addr = addr.to_string(), "starting {name} server"); + + let handle = Handle::new(); + arbiter.add_net_handle(handle.clone()).await; + + axum_server::Server::bind(addr) + .acceptor(RustlsAcceptor::new(config).acceptor(DefaultAcceptor::new())) + .handle(handle) + .serve(router.into_make_service_with_connect_info::()) + .await?; + + Ok(()) +} + +/// Start a TLS server. +/// +/// `name` is only used for observability purposes and should describe which module is starting the +/// server. +pub fn start_tls( + tasks: &mut Tasks, + name: &'static str, + router: Router, + addr: net::SocketAddr, + config: RustlsConfig, +) -> Result<()> { + let arbiter = tasks.arbiter(); + tasks + .build_task() + .name(&format!("{}::run_tls({name}, {addr})", module_path!())) + .spawn(run_tls(arbiter, name, router, addr, config))?; + Ok(()) +} From 65f33d6bdc4066d8594fe5bf5741f4144141d490 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 18:35:12 +0200 Subject: [PATCH 23/36] packages/ak-axum/accept/tls: init Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 31 +++++++++++++++++ Cargo.toml | 2 ++ packages/ak-axum/Cargo.toml | 3 ++ packages/ak-axum/src/accept/mod.rs | 1 + packages/ak-axum/src/accept/tls.rs | 54 ++++++++++++++++++++++++++++++ packages/ak-axum/src/lib.rs | 1 + packages/ak-axum/src/server.rs | 6 +++- 7 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 packages/ak-axum/src/accept/mod.rs create mode 100644 packages/ak-axum/src/accept/tls.rs diff --git a/Cargo.lock b/Cargo.lock index bebb15f70087..dbbcaee386a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,9 @@ dependencies = [ "axum-server", "durstr", "eyre", + "futures", + "tokio", + "tokio-rustls", "tower", "tower-http", "tracing", @@ -702,6 +705,21 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -709,6 +727,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -717,6 +736,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -752,6 +782,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", diff --git a/Cargo.toml b/Cargo.toml index ce859caf6595..57951ac5a8ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ config-rs = { package = "config", version = "= 0.15.22", default-features = fals dotenvy = "= 0.15.7" durstr = "= 0.5.1" eyre = "= 0.6.12" +futures = "= 0.3.32" glob = "= 0.3.3" nix = { version = "= 0.31.2", features = ["signal"] } notify = "= 8.2.0" @@ -60,6 +61,7 @@ serde_with = { version = "= 3.18.0", default-features = false, features = [ ] } tempfile = "= 3.27.0" tokio = { version = "= 1.50.0", features = ["full", "tracing"] } +tokio-rustls = "= 0.26.4" tokio-util = { version = "= 0.7.18", features = ["full"] } tower = "= 0.5.3" tower-http = { version = "= 0.6.8", features = ["timeout"] } diff --git a/packages/ak-axum/Cargo.toml b/packages/ak-axum/Cargo.toml index 243a924ee492..29efd811c5db 100644 --- a/packages/ak-axum/Cargo.toml +++ b/packages/ak-axum/Cargo.toml @@ -15,6 +15,9 @@ axum-server.workspace = true durstr.workspace = true ak-common.workspace = true eyre.workspace = true +futures.workspace = true +tokio.workspace = true +tokio-rustls.workspace = true tower.workspace = true tower-http.workspace = true tracing.workspace = true diff --git a/packages/ak-axum/src/accept/mod.rs b/packages/ak-axum/src/accept/mod.rs new file mode 100644 index 000000000000..dbdc4f3cd92a --- /dev/null +++ b/packages/ak-axum/src/accept/mod.rs @@ -0,0 +1 @@ +pub mod tls; diff --git a/packages/ak-axum/src/accept/tls.rs b/packages/ak-axum/src/accept/tls.rs new file mode 100644 index 000000000000..0fa01146a5fa --- /dev/null +++ b/packages/ak-axum/src/accept/tls.rs @@ -0,0 +1,54 @@ +use axum::{Extension, middleware::AddExtension}; +use axum_server::{accept::Accept, tls_rustls::RustlsAcceptor}; +use futures::future::BoxFuture; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_rustls::{rustls::pki_types::CertificateDer, server::TlsStream}; +use tower::Layer as _; +use tracing::instrument; + +#[derive(Clone, Debug)] +pub struct TlsState { + pub peer_certificates: Option>>, +} + +#[derive(Clone)] +pub(crate) struct TlsAcceptor { + inner: RustlsAcceptor, +} + +impl TlsAcceptor { + pub(crate) fn new(inner: RustlsAcceptor) -> Self { + Self { inner } + } +} + +impl Accept for TlsAcceptor +where + A: Accept + Clone + Send + 'static, + A::Stream: AsyncRead + AsyncWrite + Unpin + Send, + A::Service: Send, + A::Future: Send, + I: AsyncRead + AsyncWrite + Unpin + Send + 'static, + S: Send + 'static, +{ + type Future = BoxFuture<'static, std::io::Result<(Self::Stream, Self::Service)>>; + type Service = AddExtension; + type Stream = TlsStream; + + #[instrument(skip_all)] + fn accept(&self, stream: I, service: S) -> Self::Future { + let acceptor = self.inner.clone(); + + Box::pin(async move { + let (stream, service) = acceptor.accept(stream, service).await?; + let server_conn = stream.get_ref().1; + let tls_state = TlsState { + peer_certificates: server_conn.peer_certificates().map(|c| c.to_owned()), + }; + + let service = Extension(tls_state).layer(service); + + Ok((stream, service)) + }) + } +} diff --git a/packages/ak-axum/src/lib.rs b/packages/ak-axum/src/lib.rs index 836717255653..b3b345f6e365 100644 --- a/packages/ak-axum/src/lib.rs +++ b/packages/ak-axum/src/lib.rs @@ -1,4 +1,5 @@ //! Utilities for working with [`axum`]. +pub mod accept; pub mod router; pub mod server; diff --git a/packages/ak-axum/src/server.rs b/packages/ak-axum/src/server.rs index 3869fe2525f0..df0dc709d42d 100644 --- a/packages/ak-axum/src/server.rs +++ b/packages/ak-axum/src/server.rs @@ -12,6 +12,8 @@ use axum_server::{ use eyre::Result; use tracing::info; +use crate::accept::tls::TlsAcceptor; + async fn run_plain( arbiter: Arbiter, name: &str, @@ -119,7 +121,9 @@ async fn run_tls( arbiter.add_net_handle(handle.clone()).await; axum_server::Server::bind(addr) - .acceptor(RustlsAcceptor::new(config).acceptor(DefaultAcceptor::new())) + .acceptor(TlsAcceptor::new( + RustlsAcceptor::new(config).acceptor(DefaultAcceptor::new()), + )) .handle(handle) .serve(router.into_make_service_with_connect_info::()) .await?; From 291797051af1e585cd75fdb232e194df8c6cea44 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 18:40:31 +0200 Subject: [PATCH 24/36] packages/ak-axum/accept/proxy_protocol: init Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/accept/mod.rs | 1 + packages/ak-axum/src/accept/proxy_protocol.rs | 86 +++++++++++++++++++ packages/ak-axum/src/server.rs | 8 +- 3 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 packages/ak-axum/src/accept/proxy_protocol.rs diff --git a/packages/ak-axum/src/accept/mod.rs b/packages/ak-axum/src/accept/mod.rs index dbdc4f3cd92a..2f4d0266b42d 100644 --- a/packages/ak-axum/src/accept/mod.rs +++ b/packages/ak-axum/src/accept/mod.rs @@ -1 +1,2 @@ +pub mod proxy_protocol; pub mod tls; diff --git a/packages/ak-axum/src/accept/proxy_protocol.rs b/packages/ak-axum/src/accept/proxy_protocol.rs new file mode 100644 index 000000000000..2c87e7a4c665 --- /dev/null +++ b/packages/ak-axum/src/accept/proxy_protocol.rs @@ -0,0 +1,86 @@ +use std::{io, time::Duration}; + +use axum::{Extension, middleware::AddExtension}; +use axum_server::accept::{Accept, DefaultAcceptor}; +use futures::future::BoxFuture; +use tokio::io::{AsyncRead, AsyncWrite}; +use tower::Layer as _; +use tracing::instrument; + +use ak_common::tokio::proxy_protocol::{ProxyProtocolStream, header::Header}; + +#[derive(Clone, Debug)] +pub struct ProxyProtocolState { + pub header: Option>, +} + +#[derive(Clone)] +pub(crate) struct ProxyProtocolAcceptor { + inner: A, + parsing_timeout: Duration, +} + +impl ProxyProtocolAcceptor { + pub(crate) fn new() -> Self { + let inner = DefaultAcceptor::new(); + + #[cfg(not(test))] + let parsing_timeout = Duration::from_secs(10); + + // Don't force tests to wait too long + #[cfg(test)] + let parsing_timeout = Duration::from_secs(1); + + Self { + inner, + parsing_timeout, + } + } +} + +impl Default for ProxyProtocolAcceptor { + fn default() -> Self { + Self::new() + } +} + +impl ProxyProtocolAcceptor { + pub(crate) fn acceptor(self, acceptor: Acceptor) -> ProxyProtocolAcceptor { + ProxyProtocolAcceptor { + inner: acceptor, + parsing_timeout: self.parsing_timeout, + } + } +} + +impl Accept for ProxyProtocolAcceptor +where + A: Accept + Clone + Send + 'static, + A::Stream: AsyncRead + AsyncWrite + Unpin + Send, + A::Service: Send, + A::Future: Send, + I: AsyncRead + AsyncWrite + Unpin + Send + 'static, + S: Send + 'static, +{ + type Future = BoxFuture<'static, io::Result<(Self::Stream, Self::Service)>>; + type Service = AddExtension; + type Stream = ProxyProtocolStream; + + #[instrument(skip_all)] + fn accept(&self, stream: I, service: S) -> Self::Future { + let acceptor = self.inner.clone(); + + Box::pin(async move { + let (stream, service) = acceptor.accept(stream, service).await?; + let stream = ProxyProtocolStream::new(stream).await?; + + let proxy_protocol_state = ProxyProtocolState { + header: stream.header().cloned(), + }; + + let service = Extension(proxy_protocol_state).layer(service); + + Ok((stream, service)) + }) + } +} diff --git a/packages/ak-axum/src/server.rs b/packages/ak-axum/src/server.rs index df0dc709d42d..79d0ba724e34 100644 --- a/packages/ak-axum/src/server.rs +++ b/packages/ak-axum/src/server.rs @@ -12,7 +12,7 @@ use axum_server::{ use eyre::Result; use tracing::info; -use crate::accept::tls::TlsAcceptor; +use crate::accept::{proxy_protocol::ProxyProtocolAcceptor, tls::TlsAcceptor}; async fn run_plain( arbiter: Arbiter, @@ -27,7 +27,7 @@ async fn run_plain( arbiter.add_net_handle(handle.clone()).await; let res = axum_server::Server::bind(addr) - .acceptor(DefaultAcceptor::new()) + .acceptor(ProxyProtocolAcceptor::new().acceptor(DefaultAcceptor::new())) .handle(handle) .serve(router.into_make_service_with_connect_info::()) .await; @@ -121,9 +121,9 @@ async fn run_tls( arbiter.add_net_handle(handle.clone()).await; axum_server::Server::bind(addr) - .acceptor(TlsAcceptor::new( + .acceptor(ProxyProtocolAcceptor::new().acceptor(TlsAcceptor::new( RustlsAcceptor::new(config).acceptor(DefaultAcceptor::new()), - )) + ))) .handle(handle) .serve(router.into_make_service_with_connect_info::()) .await?; From 89fbf0d68b21298ab45a2afb243e771616109809 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 18:56:17 +0200 Subject: [PATCH 25/36] packages/ak-axum/extract/trusted_proxy: init Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 4 ++ Cargo.toml | 1 + packages/ak-axum/src/accept/proxy_protocol.rs | 3 +- packages/ak-axum/src/extract/mod.rs | 1 + packages/ak-axum/src/extract/trusted_proxy.rs | 59 +++++++++++++++++++ packages/ak-axum/src/lib.rs | 1 + packages/ak-axum/src/router.rs | 6 +- packages/ak-common/Cargo.toml | 1 + packages/ak-common/src/config/schema.rs | 2 + 9 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 packages/ak-axum/src/extract/mod.rs create mode 100644 packages/ak-axum/src/extract/trusted_proxy.rs diff --git a/Cargo.lock b/Cargo.lock index ba84ae6e48a9..61683172aa8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,7 @@ dependencies = [ "config", "eyre", "glob", + "ipnet", "nix", "notify", "pin-project-lite", @@ -1166,6 +1167,9 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "iri-string" diff --git a/Cargo.toml b/Cargo.toml index a4d826e673e6..e134c283670c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ durstr = "= 0.5.1" eyre = "= 0.6.12" futures = "= 0.3.32" glob = "= 0.3.3" +ipnet = { version = "2.12.0", features = ["serde"] } nix = { version = "= 0.31.2", features = ["signal"] } notify = "= 8.2.0" pin-project-lite = "= 0.2.17" diff --git a/packages/ak-axum/src/accept/proxy_protocol.rs b/packages/ak-axum/src/accept/proxy_protocol.rs index 2c87e7a4c665..b9c112c2e567 100644 --- a/packages/ak-axum/src/accept/proxy_protocol.rs +++ b/packages/ak-axum/src/accept/proxy_protocol.rs @@ -1,5 +1,6 @@ use std::{io, time::Duration}; +use ak_common::tokio::proxy_protocol::{ProxyProtocolStream, header::Header}; use axum::{Extension, middleware::AddExtension}; use axum_server::accept::{Accept, DefaultAcceptor}; use futures::future::BoxFuture; @@ -7,8 +8,6 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tower::Layer as _; use tracing::instrument; -use ak_common::tokio::proxy_protocol::{ProxyProtocolStream, header::Header}; - #[derive(Clone, Debug)] pub struct ProxyProtocolState { pub header: Option>, diff --git a/packages/ak-axum/src/extract/mod.rs b/packages/ak-axum/src/extract/mod.rs new file mode 100644 index 000000000000..e395e3494b0f --- /dev/null +++ b/packages/ak-axum/src/extract/mod.rs @@ -0,0 +1 @@ +pub mod trusted_proxy; diff --git a/packages/ak-axum/src/extract/trusted_proxy.rs b/packages/ak-axum/src/extract/trusted_proxy.rs new file mode 100644 index 000000000000..5848000cc6ef --- /dev/null +++ b/packages/ak-axum/src/extract/trusted_proxy.rs @@ -0,0 +1,59 @@ +use std::net::SocketAddr; + +use ak_common::config; +use axum::{ + Extension, RequestPartsExt as _, + extract::{ConnectInfo, FromRequestParts, Request}, + http::request::Parts, + middleware::Next, + response::Response, +}; +use tracing::{instrument, trace}; + +#[derive(Clone, Copy, Debug)] +pub struct TrustedProxy(pub bool); + +impl FromRequestParts for TrustedProxy +where + S: Send + Sync, +{ + type Rejection = as FromRequestParts>::Rejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + Extension::::from_request_parts(parts, state) + .await + .map(|Extension(trusted_proxy)| trusted_proxy) + } +} + +#[instrument(skip_all)] +async fn extract_trusted_proxy(parts: &mut Parts) -> bool { + if let Ok(ConnectInfo(addr)) = parts.extract::>().await { + let trusted_proxy_cidrs = &config::get().listen.trusted_proxy_cidrs; + + for trusted_net in trusted_proxy_cidrs { + if trusted_net.contains(&addr.ip()) { + trace!( + ?addr, + ?trusted_net, + "connection is now considered coming from a trusted proxy" + ); + return true; + } + } + } + false +} + +pub(crate) async fn trusted_proxy_middleware(request: Request, next: Next) -> Response { + let (mut parts, body) = request.into_parts(); + + let trusted_proxy = extract_trusted_proxy(&mut parts).await; + parts + .extensions + .insert::(TrustedProxy(trusted_proxy)); + + let request = Request::from_parts(parts, body); + + next.run(request).await +} diff --git a/packages/ak-axum/src/lib.rs b/packages/ak-axum/src/lib.rs index cdd68c1c4149..5a4e02aab3fa 100644 --- a/packages/ak-axum/src/lib.rs +++ b/packages/ak-axum/src/lib.rs @@ -1,6 +1,7 @@ //! Utilities for working with [`axum`]. pub mod accept; +pub mod extract; pub mod router; pub mod server; pub mod tracing; diff --git a/packages/ak-axum/src/router.rs b/packages/ak-axum/src/router.rs index a2722bd744bc..a1118a7f8c8f 100644 --- a/packages/ak-axum/src/router.rs +++ b/packages/ak-axum/src/router.rs @@ -5,7 +5,10 @@ use axum::{Router, http::StatusCode, middleware::from_fn}; use tower::ServiceBuilder; use tower_http::timeout::TimeoutLayer; -use crate::tracing::{span_middleware, tracing_middleware}; +use crate::{ + extract::trusted_proxy::trusted_proxy_middleware, + tracing::{span_middleware, tracing_middleware}, +}; /// Wrap a [`Router`] with common middlewares. /// @@ -23,6 +26,7 @@ pub fn wrap_router(router: Router, with_tracing: bool) -> Router { StatusCode::REQUEST_TIMEOUT, timeout, )) + .layer(from_fn(trusted_proxy_middleware)) .layer(from_fn(span_middleware)); if with_tracing { router.layer(service_builder.layer(from_fn(tracing_middleware))) diff --git a/packages/ak-common/Cargo.toml b/packages/ak-common/Cargo.toml index d108a5c3ab2f..c4b7c1932950 100644 --- a/packages/ak-common/Cargo.toml +++ b/packages/ak-common/Cargo.toml @@ -15,6 +15,7 @@ axum-server.workspace = true config-rs.workspace = true eyre.workspace = true glob.workspace = true +ipnet.workspace = true notify.workspace = true pin-project-lite.workspace = true serde.workspace = true diff --git a/packages/ak-common/src/config/schema.rs b/packages/ak-common/src/config/schema.rs index 29b945c27e53..826988782a2b 100644 --- a/packages/ak-common/src/config/schema.rs +++ b/packages/ak-common/src/config/schema.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, net::SocketAddr, num::NonZeroUsize}; +use ipnet::IpNet; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -48,6 +49,7 @@ pub struct ListenConfig { pub http: Vec, pub metrics: Vec, pub debug_tokio: SocketAddr, + pub trusted_proxy_cidrs: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] From e2ab302ce7c50e14b177afee159d3348afdd4aaf Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:00:34 +0200 Subject: [PATCH 26/36] add docs Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/extract/trusted_proxy.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/ak-axum/src/extract/trusted_proxy.rs b/packages/ak-axum/src/extract/trusted_proxy.rs index 5848000cc6ef..03131d9fa5b9 100644 --- a/packages/ak-axum/src/extract/trusted_proxy.rs +++ b/packages/ak-axum/src/extract/trusted_proxy.rs @@ -1,3 +1,5 @@ +//! axum extractor and middleware to check if a request comes from a trusted proxy. + use std::net::SocketAddr; use ak_common::config; @@ -10,6 +12,10 @@ use axum::{ }; use tracing::{instrument, trace}; +/// Whether the request comes from a trusted proxy. +/// +/// The [`trusted_proxy_middleware`] must be added to the router before using this extractor, +/// otherwise this will result in requests erroring. #[derive(Clone, Copy, Debug)] pub struct TrustedProxy(pub bool); @@ -26,6 +32,7 @@ where } } +/// Check whether the request comes from a trusted proxy. #[instrument(skip_all)] async fn extract_trusted_proxy(parts: &mut Parts) -> bool { if let Ok(ConnectInfo(addr)) = parts.extract::>().await { @@ -45,7 +52,10 @@ async fn extract_trusted_proxy(parts: &mut Parts) -> bool { false } -pub(crate) async fn trusted_proxy_middleware(request: Request, next: Next) -> Response { +/// Middleware required by the [`TrustedProxy`] extractor. +/// +/// Use with [`axum::middleware::from_fn`]. +pub async fn trusted_proxy_middleware(request: Request, next: Next) -> Response { let (mut parts, body) = request.into_parts(); let trusted_proxy = extract_trusted_proxy(&mut parts).await; From a30b345aeabb522bc594587a7a68b73e94c5264f Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:05:32 +0200 Subject: [PATCH 27/36] wip Signed-off-by: Marc 'risson' Schmitt --- Cargo.lock | 27 +++ Cargo.toml | 1 + packages/ak-axum/Cargo.toml | 1 + packages/ak-axum/src/extract/client_ip.rs | 237 ++++++++++++++++++++++ packages/ak-axum/src/extract/mod.rs | 1 + packages/ak-axum/src/router.rs | 5 +- 6 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 packages/ak-axum/src/extract/client_ip.rs diff --git a/Cargo.lock b/Cargo.lock index 61683172aa8b..fa6391c8d9bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,7 @@ dependencies = [ "authentik-common", "axum", "axum-server", + "client-ip", "durstr", "eyre", "futures", @@ -449,6 +450,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "client-ip" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39d2056bf065c8b4bce5a8898d40e175211ff4410add2a84d695845d3937c729" +dependencies = [ + "forwarded-header-value", + "http", +] + [[package]] name = "cmake" version = "0.1.57" @@ -683,6 +694,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror 1.0.69", +] + [[package]] name = "fs-err" version = "3.3.0" @@ -1409,6 +1430,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "notify" version = "8.2.0" diff --git a/Cargo.toml b/Cargo.toml index e134c283670c..3b9efc75bbc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] } aws-lc-rs = { version = "= 1.16.2", features = ["fips"] } axum = { version = "= 0.8.8", features = ["http2", "macros", "ws"] } clap = { version = "= 4.6.0", features = ["derive", "env"] } +client-ip = { version = "0.2.1", features = ["forwarded-header"] } colored = "= 3.1.1" config-rs = { package = "config", version = "= 0.15.22", default-features = false, features = [ "yaml", diff --git a/packages/ak-axum/Cargo.toml b/packages/ak-axum/Cargo.toml index ce4872eb4473..3eee20cc9704 100644 --- a/packages/ak-axum/Cargo.toml +++ b/packages/ak-axum/Cargo.toml @@ -13,6 +13,7 @@ publish.workspace = true ak-common.workspace = true axum-server.workspace = true axum.workspace = true +client-ip.workspace = true durstr.workspace = true eyre.workspace = true futures.workspace = true diff --git a/packages/ak-axum/src/extract/client_ip.rs b/packages/ak-axum/src/extract/client_ip.rs new file mode 100644 index 000000000000..698f512205b2 --- /dev/null +++ b/packages/ak-axum/src/extract/client_ip.rs @@ -0,0 +1,237 @@ +//! axum extractor and middleware to retrieve the client IP. + +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; + +use axum::{ + Extension, RequestPartsExt as _, + extract::{ConnectInfo, FromRequestParts, Request}, + http::request::Parts, + middleware::Next, + response::Response, +}; +use tracing::{Span, instrument}; + +use crate::{accept::proxy_protocol::ProxyProtocolState, extract::trusted_proxy::TrustedProxy}; + +/// Client IP. +/// +/// The [`client_ip_middleware`] must be added to the router before using this extractor, +/// otherwise this will result in requests erroring. +#[derive(Clone, Copy, Debug)] +pub struct ClientIp(pub IpAddr); + +impl FromRequestParts for ClientIp +where + S: Send + Sync, +{ + type Rejection = as FromRequestParts>::Rejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + Extension::::from_request_parts(parts, state) + .await + .map(|Extension(client_ip)| client_ip) + } +} + +/// Get the client IP from the request. +#[instrument(skip_all)] +async fn extract_client_ip(parts: &mut Parts) -> IpAddr { + let is_trusted = parts + .extract::() + .await + .unwrap_or(TrustedProxy(false)) + .0; + + if is_trusted { + if let Ok(ip) = client_ip::rightmost_x_forwarded_for(&parts.headers) { + return ip; + } + + if let Ok(ip) = client_ip::x_real_ip(&parts.headers) { + return ip; + } + + if let Ok(ip) = client_ip::rightmost_forwarded(&parts.headers) { + return ip; + } + + if let Ok(Extension(proxy_protocol_state)) = + parts.extract::>().await + && let Some(header) = &proxy_protocol_state.header + && let Some(addr) = header.proxied_address() + { + return addr.source.ip(); + } + } + + if let Ok(ConnectInfo(addr)) = parts.extract::>().await { + addr.ip() + } else { + // No connect info means we received a request via a Unix socket, hence localhost + // as default. + Ipv6Addr::LOCALHOST.into() + } +} + +/// Middleware required by the [`ClientIp`] extractor. +/// +/// Use with [`axum::middleware::from_fn`]. +pub async fn client_ip_middleware(request: Request, next: Next) -> Response { + let (mut parts, body) = request.into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + Span::current().record("remote", client_ip.to_string()); + parts.extensions.insert::(ClientIp(client_ip)); + + let request = Request::from_parts(parts, body); + + next.run(request).await +} + +#[cfg(test)] +mod tests { + use std::net::Ipv4Addr; + + use axum::{body::Body, http::Request}; + + use super::*; + + #[tokio::test] + async fn x_forwarded_for_trusted() { + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .header("x-forwarded-for", "192.0.2.51, 192.0.2.42") + .extension(TrustedProxy(true)) + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 42),); + } + + #[tokio::test] + async fn x_real_ip_trusted() { + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .header("x-real-ip", "192.0.2.42") + .extension(TrustedProxy(true)) + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 42),); + } + + #[tokio::test] + async fn forwarded_header_trusted() { + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .header("forwarded", "for=192.0.2.42") + .extension(TrustedProxy(true)) + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 42),); + } + + #[tokio::test] + async fn from_connect_info() { + let connect_addr: SocketAddr = "192.0.2.42:34932" + .parse() + .expect("Failed to parse socket address"); + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .extension(ConnectInfo(connect_addr)) + .extension(TrustedProxy(false)) + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 42),); + } + + #[tokio::test] + async fn headers_untrusted() { + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .header("x-forwarded-for", "192.0.2.42") + .extension(TrustedProxy(false)) + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv6Addr::LOCALHOST); + } + + #[tokio::test] + async fn priority_order() { + // Test that X-Forwarded-For takes priority over other headers when trusted + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .header("x-forwarded-for", "192.0.2.1") + .header("x-real-ip", "192.0.2.2") + .header("forwarded", "for=192.0.2.3") + .extension(TrustedProxy(true)) + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 1),); + } + + #[tokio::test] + async fn no_ip_found() { + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv6Addr::LOCALHOST); + } + + #[tokio::test] + async fn ipv6() { + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .header("x-forwarded-for", "2001:db8::42") + .extension(TrustedProxy(true)) + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x42),); + } + + #[tokio::test] + async fn multiple_x_forwarded_for() { + let (mut parts, _) = Request::builder() + .uri("http://example.com/path") + .header("x-forwarded-for", "192.0.2.1, 192.0.2.2, 192.0.2.3") + .extension(TrustedProxy(true)) + .body(Body::empty()) + .expect("Failed to create request") + .into_parts(); + + let client_ip = extract_client_ip(&mut parts).await; + + assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 3),); + } +} diff --git a/packages/ak-axum/src/extract/mod.rs b/packages/ak-axum/src/extract/mod.rs index e395e3494b0f..23da8fe1a0c1 100644 --- a/packages/ak-axum/src/extract/mod.rs +++ b/packages/ak-axum/src/extract/mod.rs @@ -1 +1,2 @@ +pub mod client_ip; pub mod trusted_proxy; diff --git a/packages/ak-axum/src/router.rs b/packages/ak-axum/src/router.rs index a1118a7f8c8f..a86cb70c7237 100644 --- a/packages/ak-axum/src/router.rs +++ b/packages/ak-axum/src/router.rs @@ -6,7 +6,7 @@ use tower::ServiceBuilder; use tower_http::timeout::TimeoutLayer; use crate::{ - extract::trusted_proxy::trusted_proxy_middleware, + extract::{client_ip::client_ip_middleware, trusted_proxy::trusted_proxy_middleware}, tracing::{span_middleware, tracing_middleware}, }; @@ -26,8 +26,9 @@ pub fn wrap_router(router: Router, with_tracing: bool) -> Router { StatusCode::REQUEST_TIMEOUT, timeout, )) + .layer(from_fn(span_middleware)) .layer(from_fn(trusted_proxy_middleware)) - .layer(from_fn(span_middleware)); + .layer(from_fn(client_ip_middleware)); if with_tracing { router.layer(service_builder.layer(from_fn(tracing_middleware))) } else { From 267336f7954b149e5a2ecfdc449a51929dc22862 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:05:50 +0200 Subject: [PATCH 28/36] fix layer order Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/router.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ak-axum/src/router.rs b/packages/ak-axum/src/router.rs index a1118a7f8c8f..6f678a85b31b 100644 --- a/packages/ak-axum/src/router.rs +++ b/packages/ak-axum/src/router.rs @@ -26,8 +26,8 @@ pub fn wrap_router(router: Router, with_tracing: bool) -> Router { StatusCode::REQUEST_TIMEOUT, timeout, )) - .layer(from_fn(trusted_proxy_middleware)) - .layer(from_fn(span_middleware)); + .layer(from_fn(span_middleware)) + .layer(from_fn(trusted_proxy_middleware)); if with_tracing { router.layer(service_builder.layer(from_fn(tracing_middleware))) } else { From 5e036104e0a272f37cc28d81f2c98020bd3cd020 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:08:20 +0200 Subject: [PATCH 29/36] fixup Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/tracing.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/ak-axum/src/tracing.rs b/packages/ak-axum/src/tracing.rs index c69678aa31b4..61805746b55b 100644 --- a/packages/ak-axum/src/tracing.rs +++ b/packages/ak-axum/src/tracing.rs @@ -27,9 +27,6 @@ pub(crate) async fn span_middleware(request: Request, next: Next) -> Response { "request", path = %request.uri(), method = %request.method(), - remote = field::Empty, - scheme = field::Empty, - host = field::Empty, http_headers = ?http_headers, ); next.run(request).instrument(span).await From c2bd441948c18d7d92af043166bd9a00795a7b2b Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:09:25 +0200 Subject: [PATCH 30/36] fixup Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/tracing.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ak-axum/src/tracing.rs b/packages/ak-axum/src/tracing.rs index 61805746b55b..5f2bf196fee5 100644 --- a/packages/ak-axum/src/tracing.rs +++ b/packages/ak-axum/src/tracing.rs @@ -27,6 +27,7 @@ pub(crate) async fn span_middleware(request: Request, next: Next) -> Response { "request", path = %request.uri(), method = %request.method(), + remote = field::Empty, http_headers = ?http_headers, ); next.run(request).instrument(span).await From 5b13d5b8382d54e28970a2b768543e4baa9a932b Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:09:43 +0200 Subject: [PATCH 31/36] fixup Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/tracing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ak-axum/src/tracing.rs b/packages/ak-axum/src/tracing.rs index 61805746b55b..3fd8a989e9bc 100644 --- a/packages/ak-axum/src/tracing.rs +++ b/packages/ak-axum/src/tracing.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use ak_common::config; use axum::{extract::Request, middleware::Next, response::Response}; use tokio::time::Instant; -use tracing::{Instrument as _, field, info, info_span, trace}; +use tracing::{Instrument as _, info, info_span, trace}; /// Create a [`tracing::Span`] for requests. pub(crate) async fn span_middleware(request: Request, next: Next) -> Response { From 4c84c53f097fdb069ca0216d1c94c39e4bb0fb53 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:10:10 +0200 Subject: [PATCH 32/36] fixup Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/tracing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ak-axum/src/tracing.rs b/packages/ak-axum/src/tracing.rs index 597762d8ae2d..5f2bf196fee5 100644 --- a/packages/ak-axum/src/tracing.rs +++ b/packages/ak-axum/src/tracing.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use ak_common::config; use axum::{extract::Request, middleware::Next, response::Response}; use tokio::time::Instant; -use tracing::{Instrument as _, info, info_span, trace}; +use tracing::{Instrument as _, field, info, info_span, trace}; /// Create a [`tracing::Span`] for requests. pub(crate) async fn span_middleware(request: Request, next: Next) -> Response { From 703d5af89b33072ac1dd671f9989eb755b1541a7 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:12:58 +0200 Subject: [PATCH 33/36] fixup Signed-off-by: Marc 'risson' Schmitt --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e134c283670c..ee7ed916f5f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ durstr = "= 0.5.1" eyre = "= 0.6.12" futures = "= 0.3.32" glob = "= 0.3.3" -ipnet = { version = "2.12.0", features = ["serde"] } +ipnet = { version = "= 2.12.0", features = ["serde"] } nix = { version = "= 0.31.2", features = ["signal"] } notify = "= 8.2.0" pin-project-lite = "= 0.2.17" From da38234d612527cf1399425b2943672b979d93fc Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:16:30 +0200 Subject: [PATCH 34/36] add doc Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/extract/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ak-axum/src/extract/mod.rs b/packages/ak-axum/src/extract/mod.rs index e395e3494b0f..2888d8d37235 100644 --- a/packages/ak-axum/src/extract/mod.rs +++ b/packages/ak-axum/src/extract/mod.rs @@ -1 +1,3 @@ +//! axum extractors to get information about a request. + pub mod trusted_proxy; From a522b8be76a7e2731f87b1a6b5ebcb629714f912 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 1 Apr 2026 19:46:57 +0200 Subject: [PATCH 35/36] fmt Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/accept/proxy_protocol.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ak-axum/src/accept/proxy_protocol.rs b/packages/ak-axum/src/accept/proxy_protocol.rs index 2c87e7a4c665..b9c112c2e567 100644 --- a/packages/ak-axum/src/accept/proxy_protocol.rs +++ b/packages/ak-axum/src/accept/proxy_protocol.rs @@ -1,5 +1,6 @@ use std::{io, time::Duration}; +use ak_common::tokio::proxy_protocol::{ProxyProtocolStream, header::Header}; use axum::{Extension, middleware::AddExtension}; use axum_server::accept::{Accept, DefaultAcceptor}; use futures::future::BoxFuture; @@ -7,8 +8,6 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tower::Layer as _; use tracing::instrument; -use ak_common::tokio::proxy_protocol::{ProxyProtocolStream, header::Header}; - #[derive(Clone, Debug)] pub struct ProxyProtocolState { pub header: Option>, From 75fc6dff217bc2afddd29d842e4f2d030beb9fa3 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 8 Apr 2026 13:44:51 +0200 Subject: [PATCH 36/36] fixup Signed-off-by: Marc 'risson' Schmitt --- packages/ak-axum/src/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ak-axum/src/router.rs b/packages/ak-axum/src/router.rs index b09cc675ba9e..a2722bd744bc 100644 --- a/packages/ak-axum/src/router.rs +++ b/packages/ak-axum/src/router.rs @@ -1,7 +1,7 @@ //! Utilities for working with [`Router`]. use ak_common::config; -use axum::{http::StatusCode, middleware::from_fn, Router}; +use axum::{Router, http::StatusCode, middleware::from_fn}; use tower::ServiceBuilder; use tower_http::timeout::TimeoutLayer;