From b8add810185ffc30221e32601f6b065e04a97fed Mon Sep 17 00:00:00 2001 From: artemws <59208085+artemws@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:18:09 +0200 Subject: [PATCH] Implement hot-reload for config and log level Added hot-reload functionality for configuration and log level. --- src/main.rs | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 320c2c5..d8ab79e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ mod tls_front; mod util; use crate::config::{LogLevel, ProxyConfig}; +use crate::config::hot_reload::spawn_config_watcher; use crate::crypto::SecureRandom; use crate::ip_tracker::UserIpTracker; use crate::network::probe::{decide_network_capabilities, log_probe_result, run_probe}; @@ -469,6 +470,16 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai // Freeze config after possible fallback decision let config = Arc::new(config); + // ── Hot-reload watcher ──────────────────────────────────────────────── + // Spawns a background task that watches the config file and reloads it + // on SIGHUP (Unix) or every 60 seconds. Each accept-loop clones the + // receiver and calls `.borrow_and_update().clone()` per connection. + let (config_rx, mut log_level_rx) = spawn_config_watcher( + std::path::PathBuf::from(&config_path), + config.clone(), + std::time::Duration::from_secs(60), + ); + let replay_checker = Arc::new(ReplayChecker::new( config.access.replay_check_len, Duration::from_secs(config.access.replay_window_secs), @@ -760,7 +771,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai has_unix_listener = true; - let config = config.clone(); + let mut config_rx_unix = config_rx.clone(); let stats = stats.clone(); let upstream_manager = upstream_manager.clone(); let replay_checker = replay_checker.clone(); @@ -779,7 +790,8 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai let conn_id = unix_conn_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let fake_peer = SocketAddr::from(([127, 0, 0, 1], (conn_id % 65535) as u16)); - let config = config.clone(); + // Pick up the latest config atomically for this connection. + let config = config_rx_unix.borrow_and_update().clone(); let stats = stats.clone(); let upstream_manager = upstream_manager.clone(); let replay_checker = replay_checker.clone(); @@ -813,7 +825,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai std::process::exit(1); } - // Switch to user-configured log level after startup + // Switch to user-configured log level after startup (before starting listeners) let runtime_filter = if has_rust_log { EnvFilter::from_default_env() } else if matches!(effective_log_level, LogLevel::Silent) { @@ -825,6 +837,22 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai .reload(runtime_filter) .expect("Failed to switch log filter"); + // Apply log_level changes to the tracing reload handle. + // Note: the initial runtime_filter is applied below (after startup); this + // task handles subsequent hot-reload changes only. + tokio::spawn(async move { + loop { + if log_level_rx.changed().await.is_err() { + break; + } + let level = log_level_rx.borrow_and_update().clone(); + let new_filter = tracing_subscriber::EnvFilter::new(level.to_filter_str()); + if let Err(e) = filter_handle.reload(new_filter) { + tracing::error!("config reload: failed to update log filter: {}", e); + } + } + }); + if let Some(port) = config.server.metrics_port { let stats = stats.clone(); let whitelist = config.server.metrics_whitelist.clone(); @@ -834,7 +862,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai } for listener in listeners { - let config = config.clone(); + let mut config_rx = config_rx.clone(); let stats = stats.clone(); let upstream_manager = upstream_manager.clone(); let replay_checker = replay_checker.clone(); @@ -848,7 +876,8 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai loop { match listener.accept().await { Ok((stream, peer_addr)) => { - let config = config.clone(); + // Pick up the latest config atomically for this connection. + let config = config_rx.borrow_and_update().clone(); let stats = stats.clone(); let upstream_manager = upstream_manager.clone(); let replay_checker = replay_checker.clone();