feat: implement Unix domain socket admin API for config reload and status

Add admin socket module that binds to a configurable Unix domain socket
path (default /run/reverse-proxy/admin.sock) supporting reload and status
commands. Reload re-reads config and swaps DynamicConfig via ArcSwap with
serialized access using the same Mutex as SIGHUP. Status returns uptime
and site count. Unknown commands and invalid input return structured
JSON error responses. Stale socket files are removed at startup; if the
socket is occupied by another process, a warning is logged and the socket
is disabled. Empty admin_socket_path disables the socket entirely.

Also adds FullConfig struct to config module for parsing complete config
files during reload, and adds serde_json dependency for JSON responses.
This commit is contained in:
2026-06-11 13:13:15 +00:00
parent f1cada010f
commit 56eda4e47c
6 changed files with 710 additions and 18 deletions

View File

@@ -9,3 +9,50 @@ pub use dynamic_config::{
};
pub use static_config::{ListenerConfig, LoggingConfig, StaticConfig, TlsConfig};
pub use validation::{validate, ValidationError};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct FullConfig {
#[serde(default)]
pub listeners: Vec<ListenerConfig>,
#[serde(default)]
pub allow_wildcard_bind: bool,
#[serde(default = "static_config::default_health_check_port")]
pub health_check_port: u16,
#[serde(default = "static_config::default_admin_socket_path")]
pub admin_socket_path: String,
#[serde(default = "static_config::default_shutdown_timeout_secs")]
pub shutdown_timeout_secs: u64,
#[serde(default)]
pub logging: LoggingConfig,
pub rate_limit: RateLimitConfig,
pub body: BodyConfig,
}
impl FullConfig {
pub fn parse(content: &str) -> anyhow::Result<Self> {
Ok(toml::from_str(content)?)
}
pub fn into_static_and_dynamic(self) -> (StaticConfig, DynamicConfig) {
let static_config = StaticConfig {
listeners: self.listeners,
allow_wildcard_bind: self.allow_wildcard_bind,
health_check_port: self.health_check_port,
admin_socket_path: self.admin_socket_path,
shutdown_timeout_secs: self.shutdown_timeout_secs,
logging: self.logging,
};
let dynamic_config = DynamicConfig::from_sites(
static_config
.listeners
.iter()
.flat_map(|l| l.sites.clone())
.collect(),
self.rate_limit,
self.body,
);
(static_config, dynamic_config)
}
}

View File

@@ -16,18 +16,15 @@ pub struct StaticConfig {
pub logging: LoggingConfig,
}
#[allow(dead_code)]
fn default_health_check_port() -> u16 {
pub fn default_health_check_port() -> u16 {
9900
}
#[allow(dead_code)]
fn default_admin_socket_path() -> String {
pub fn default_admin_socket_path() -> String {
"/run/reverse-proxy/admin.sock".to_string()
}
#[allow(dead_code)]
fn default_shutdown_timeout_secs() -> u64 {
pub fn default_shutdown_timeout_secs() -> u64 {
30
}