mirror of https://github.com/telemt/telemt.git
Hot-Reload fixes
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
parent
4ae4ca8ca8
commit
a9209fd3c7
|
|
@ -9,20 +9,17 @@
|
||||||
//! | `general` | `log_level` | Filter updated via `log_level_tx` |
|
//! | `general` | `log_level` | Filter updated via `log_level_tx` |
|
||||||
//! | `access` | `user_ad_tags` | Passed on next connection |
|
//! | `access` | `user_ad_tags` | Passed on next connection |
|
||||||
//! | `general` | `ad_tag` | Passed on next connection (fallback per-user) |
|
//! | `general` | `ad_tag` | Passed on next connection (fallback per-user) |
|
||||||
//! | `general` | `middle_proxy_pool_size` | Passed on next connection |
|
|
||||||
//! | `general` | `me_keepalive_*` | Passed on next connection |
|
|
||||||
//! | `general` | `desync_all_full` | Applied immediately |
|
//! | `general` | `desync_all_full` | Applied immediately |
|
||||||
//! | `general` | `update_every` | Applied to ME updater immediately |
|
//! | `general` | `update_every` | Applied to ME updater immediately |
|
||||||
//! | `general` | `hardswap` | Applied on next ME map update |
|
//! | `general` | `me_reinit_*` | Applied to ME reinit scheduler immediately |
|
||||||
//! | `general` | `me_pool_drain_ttl_secs` | Applied on next ME map update |
|
//! | `general` | `hardswap` / `me_*_reinit` | Applied on next ME map update |
|
||||||
//! | `general` | `me_pool_min_fresh_ratio` | Applied on next ME map update |
|
|
||||||
//! | `general` | `me_reinit_drain_timeout_secs` | Applied on next ME map update |
|
|
||||||
//! | `general` | `telemetry` / `me_*_policy` | Applied immediately |
|
//! | `general` | `telemetry` / `me_*_policy` | Applied immediately |
|
||||||
//! | `network` | `dns_overrides` | Applied immediately |
|
//! | `network` | `dns_overrides` | Applied immediately |
|
||||||
//! | `access` | All user/quota fields | Effective immediately |
|
//! | `access` | All user/quota fields | Effective immediately |
|
||||||
//!
|
//!
|
||||||
//! Fields that require re-binding sockets (`server.port`, `censorship.*`,
|
//! Fields that require re-binding sockets (`server.port`, `censorship.*`,
|
||||||
//! `network.*`, `use_middle_proxy`) are **not** applied; a warning is emitted.
|
//! `network.*`, `use_middle_proxy`) are **not** applied; a warning is emitted.
|
||||||
|
//! Non-hot changes are never mixed into the runtime config snapshot.
|
||||||
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -32,7 +29,7 @@ use notify::{EventKind, RecursiveMode, Watcher, recommended_watcher};
|
||||||
use tokio::sync::{mpsc, watch};
|
use tokio::sync::{mpsc, watch};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::config::{LogLevel, MeFloorMode, MeSocksKdfPolicy, MeTelemetryLevel};
|
use crate::config::{LogLevel, MeBindStaleMode, MeFloorMode, MeSocksKdfPolicy, MeTelemetryLevel};
|
||||||
use super::load::ProxyConfig;
|
use super::load::ProxyConfig;
|
||||||
|
|
||||||
// ── Hot fields ────────────────────────────────────────────────────────────────
|
// ── Hot fields ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -43,17 +40,37 @@ pub struct HotFields {
|
||||||
pub log_level: LogLevel,
|
pub log_level: LogLevel,
|
||||||
pub ad_tag: Option<String>,
|
pub ad_tag: Option<String>,
|
||||||
pub dns_overrides: Vec<String>,
|
pub dns_overrides: Vec<String>,
|
||||||
pub middle_proxy_pool_size: usize,
|
|
||||||
pub desync_all_full: bool,
|
pub desync_all_full: bool,
|
||||||
pub update_every_secs: u64,
|
pub update_every_secs: u64,
|
||||||
|
pub me_reinit_every_secs: u64,
|
||||||
|
pub me_reinit_singleflight: bool,
|
||||||
|
pub me_reinit_coalesce_window_ms: u64,
|
||||||
pub hardswap: bool,
|
pub hardswap: bool,
|
||||||
pub me_pool_drain_ttl_secs: u64,
|
pub me_pool_drain_ttl_secs: u64,
|
||||||
pub me_pool_min_fresh_ratio: f32,
|
pub me_pool_min_fresh_ratio: f32,
|
||||||
pub me_reinit_drain_timeout_secs: u64,
|
pub me_reinit_drain_timeout_secs: u64,
|
||||||
pub me_keepalive_enabled: bool,
|
pub me_hardswap_warmup_delay_min_ms: u64,
|
||||||
pub me_keepalive_interval_secs: u64,
|
pub me_hardswap_warmup_delay_max_ms: u64,
|
||||||
pub me_keepalive_jitter_secs: u64,
|
pub me_hardswap_warmup_extra_passes: u8,
|
||||||
pub me_keepalive_payload_random: bool,
|
pub me_hardswap_warmup_pass_backoff_base_ms: u64,
|
||||||
|
pub me_bind_stale_mode: MeBindStaleMode,
|
||||||
|
pub me_bind_stale_ttl_secs: u64,
|
||||||
|
pub me_secret_atomic_snapshot: bool,
|
||||||
|
pub me_deterministic_writer_sort: bool,
|
||||||
|
pub me_single_endpoint_shadow_writers: u8,
|
||||||
|
pub me_single_endpoint_outage_mode_enabled: bool,
|
||||||
|
pub me_single_endpoint_outage_disable_quarantine: bool,
|
||||||
|
pub me_single_endpoint_outage_backoff_min_ms: u64,
|
||||||
|
pub me_single_endpoint_outage_backoff_max_ms: u64,
|
||||||
|
pub me_single_endpoint_shadow_rotate_every_secs: u64,
|
||||||
|
pub me_config_stable_snapshots: u8,
|
||||||
|
pub me_config_apply_cooldown_secs: u64,
|
||||||
|
pub me_snapshot_require_http_2xx: bool,
|
||||||
|
pub me_snapshot_reject_empty_map: bool,
|
||||||
|
pub me_snapshot_min_proxy_for_lines: u32,
|
||||||
|
pub proxy_secret_stable_snapshots: u8,
|
||||||
|
pub proxy_secret_rotate_runtime: bool,
|
||||||
|
pub proxy_secret_len_max: usize,
|
||||||
pub telemetry_core_enabled: bool,
|
pub telemetry_core_enabled: bool,
|
||||||
pub telemetry_user_enabled: bool,
|
pub telemetry_user_enabled: bool,
|
||||||
pub telemetry_me_level: MeTelemetryLevel,
|
pub telemetry_me_level: MeTelemetryLevel,
|
||||||
|
|
@ -65,7 +82,14 @@ pub struct HotFields {
|
||||||
pub me_route_backpressure_base_timeout_ms: u64,
|
pub me_route_backpressure_base_timeout_ms: u64,
|
||||||
pub me_route_backpressure_high_timeout_ms: u64,
|
pub me_route_backpressure_high_timeout_ms: u64,
|
||||||
pub me_route_backpressure_high_watermark_pct: u8,
|
pub me_route_backpressure_high_watermark_pct: u8,
|
||||||
pub access: crate::config::AccessConfig,
|
pub users: std::collections::HashMap<String, String>,
|
||||||
|
pub user_ad_tags: std::collections::HashMap<String, String>,
|
||||||
|
pub user_max_tcp_conns: std::collections::HashMap<String, usize>,
|
||||||
|
pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub user_data_quota: std::collections::HashMap<String, u64>,
|
||||||
|
pub user_max_unique_ips: std::collections::HashMap<String, usize>,
|
||||||
|
pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode,
|
||||||
|
pub user_max_unique_ips_window_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HotFields {
|
impl HotFields {
|
||||||
|
|
@ -74,17 +98,49 @@ impl HotFields {
|
||||||
log_level: cfg.general.log_level.clone(),
|
log_level: cfg.general.log_level.clone(),
|
||||||
ad_tag: cfg.general.ad_tag.clone(),
|
ad_tag: cfg.general.ad_tag.clone(),
|
||||||
dns_overrides: cfg.network.dns_overrides.clone(),
|
dns_overrides: cfg.network.dns_overrides.clone(),
|
||||||
middle_proxy_pool_size: cfg.general.middle_proxy_pool_size,
|
|
||||||
desync_all_full: cfg.general.desync_all_full,
|
desync_all_full: cfg.general.desync_all_full,
|
||||||
update_every_secs: cfg.general.effective_update_every_secs(),
|
update_every_secs: cfg.general.effective_update_every_secs(),
|
||||||
|
me_reinit_every_secs: cfg.general.me_reinit_every_secs,
|
||||||
|
me_reinit_singleflight: cfg.general.me_reinit_singleflight,
|
||||||
|
me_reinit_coalesce_window_ms: cfg.general.me_reinit_coalesce_window_ms,
|
||||||
hardswap: cfg.general.hardswap,
|
hardswap: cfg.general.hardswap,
|
||||||
me_pool_drain_ttl_secs: cfg.general.me_pool_drain_ttl_secs,
|
me_pool_drain_ttl_secs: cfg.general.me_pool_drain_ttl_secs,
|
||||||
me_pool_min_fresh_ratio: cfg.general.me_pool_min_fresh_ratio,
|
me_pool_min_fresh_ratio: cfg.general.me_pool_min_fresh_ratio,
|
||||||
me_reinit_drain_timeout_secs: cfg.general.me_reinit_drain_timeout_secs,
|
me_reinit_drain_timeout_secs: cfg.general.me_reinit_drain_timeout_secs,
|
||||||
me_keepalive_enabled: cfg.general.me_keepalive_enabled,
|
me_hardswap_warmup_delay_min_ms: cfg.general.me_hardswap_warmup_delay_min_ms,
|
||||||
me_keepalive_interval_secs: cfg.general.me_keepalive_interval_secs,
|
me_hardswap_warmup_delay_max_ms: cfg.general.me_hardswap_warmup_delay_max_ms,
|
||||||
me_keepalive_jitter_secs: cfg.general.me_keepalive_jitter_secs,
|
me_hardswap_warmup_extra_passes: cfg.general.me_hardswap_warmup_extra_passes,
|
||||||
me_keepalive_payload_random: cfg.general.me_keepalive_payload_random,
|
me_hardswap_warmup_pass_backoff_base_ms: cfg
|
||||||
|
.general
|
||||||
|
.me_hardswap_warmup_pass_backoff_base_ms,
|
||||||
|
me_bind_stale_mode: cfg.general.me_bind_stale_mode,
|
||||||
|
me_bind_stale_ttl_secs: cfg.general.me_bind_stale_ttl_secs,
|
||||||
|
me_secret_atomic_snapshot: cfg.general.me_secret_atomic_snapshot,
|
||||||
|
me_deterministic_writer_sort: cfg.general.me_deterministic_writer_sort,
|
||||||
|
me_single_endpoint_shadow_writers: cfg.general.me_single_endpoint_shadow_writers,
|
||||||
|
me_single_endpoint_outage_mode_enabled: cfg
|
||||||
|
.general
|
||||||
|
.me_single_endpoint_outage_mode_enabled,
|
||||||
|
me_single_endpoint_outage_disable_quarantine: cfg
|
||||||
|
.general
|
||||||
|
.me_single_endpoint_outage_disable_quarantine,
|
||||||
|
me_single_endpoint_outage_backoff_min_ms: cfg
|
||||||
|
.general
|
||||||
|
.me_single_endpoint_outage_backoff_min_ms,
|
||||||
|
me_single_endpoint_outage_backoff_max_ms: cfg
|
||||||
|
.general
|
||||||
|
.me_single_endpoint_outage_backoff_max_ms,
|
||||||
|
me_single_endpoint_shadow_rotate_every_secs: cfg
|
||||||
|
.general
|
||||||
|
.me_single_endpoint_shadow_rotate_every_secs,
|
||||||
|
me_config_stable_snapshots: cfg.general.me_config_stable_snapshots,
|
||||||
|
me_config_apply_cooldown_secs: cfg.general.me_config_apply_cooldown_secs,
|
||||||
|
me_snapshot_require_http_2xx: cfg.general.me_snapshot_require_http_2xx,
|
||||||
|
me_snapshot_reject_empty_map: cfg.general.me_snapshot_reject_empty_map,
|
||||||
|
me_snapshot_min_proxy_for_lines: cfg.general.me_snapshot_min_proxy_for_lines,
|
||||||
|
proxy_secret_stable_snapshots: cfg.general.proxy_secret_stable_snapshots,
|
||||||
|
proxy_secret_rotate_runtime: cfg.general.proxy_secret_rotate_runtime,
|
||||||
|
proxy_secret_len_max: cfg.general.proxy_secret_len_max,
|
||||||
telemetry_core_enabled: cfg.general.telemetry.core_enabled,
|
telemetry_core_enabled: cfg.general.telemetry.core_enabled,
|
||||||
telemetry_user_enabled: cfg.general.telemetry.user_enabled,
|
telemetry_user_enabled: cfg.general.telemetry.user_enabled,
|
||||||
telemetry_me_level: cfg.general.telemetry.me_level,
|
telemetry_me_level: cfg.general.telemetry.me_level,
|
||||||
|
|
@ -100,16 +156,149 @@ impl HotFields {
|
||||||
me_route_backpressure_base_timeout_ms: cfg.general.me_route_backpressure_base_timeout_ms,
|
me_route_backpressure_base_timeout_ms: cfg.general.me_route_backpressure_base_timeout_ms,
|
||||||
me_route_backpressure_high_timeout_ms: cfg.general.me_route_backpressure_high_timeout_ms,
|
me_route_backpressure_high_timeout_ms: cfg.general.me_route_backpressure_high_timeout_ms,
|
||||||
me_route_backpressure_high_watermark_pct: cfg.general.me_route_backpressure_high_watermark_pct,
|
me_route_backpressure_high_watermark_pct: cfg.general.me_route_backpressure_high_watermark_pct,
|
||||||
access: cfg.access.clone(),
|
users: cfg.access.users.clone(),
|
||||||
|
user_ad_tags: cfg.access.user_ad_tags.clone(),
|
||||||
|
user_max_tcp_conns: cfg.access.user_max_tcp_conns.clone(),
|
||||||
|
user_expirations: cfg.access.user_expirations.clone(),
|
||||||
|
user_data_quota: cfg.access.user_data_quota.clone(),
|
||||||
|
user_max_unique_ips: cfg.access.user_max_unique_ips.clone(),
|
||||||
|
user_max_unique_ips_mode: cfg.access.user_max_unique_ips_mode,
|
||||||
|
user_max_unique_ips_window_secs: cfg.access.user_max_unique_ips_window_secs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn canonicalize_json(value: &mut serde_json::Value) {
|
||||||
|
match value {
|
||||||
|
serde_json::Value::Object(map) => {
|
||||||
|
let mut pairs: Vec<(String, serde_json::Value)> =
|
||||||
|
std::mem::take(map).into_iter().collect();
|
||||||
|
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
for (_, item) in pairs.iter_mut() {
|
||||||
|
canonicalize_json(item);
|
||||||
|
}
|
||||||
|
for (key, item) in pairs {
|
||||||
|
map.insert(key, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::Value::Array(items) => {
|
||||||
|
for item in items {
|
||||||
|
canonicalize_json(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_equal(lhs: &ProxyConfig, rhs: &ProxyConfig) -> bool {
|
||||||
|
let mut left = match serde_json::to_value(lhs) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
let mut right = match serde_json::to_value(rhs) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
canonicalize_json(&mut left);
|
||||||
|
canonicalize_json(&mut right);
|
||||||
|
left == right
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listeners_equal(
|
||||||
|
lhs: &[crate::config::ListenerConfig],
|
||||||
|
rhs: &[crate::config::ListenerConfig],
|
||||||
|
) -> bool {
|
||||||
|
if lhs.len() != rhs.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lhs.iter().zip(rhs.iter()).all(|(a, b)| {
|
||||||
|
a.ip == b.ip
|
||||||
|
&& a.announce == b.announce
|
||||||
|
&& a.announce_ip == b.announce_ip
|
||||||
|
&& a.proxy_protocol == b.proxy_protocol
|
||||||
|
&& a.reuse_allow == b.reuse_allow
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
||||||
|
let mut cfg = old.clone();
|
||||||
|
|
||||||
|
cfg.general.log_level = new.general.log_level.clone();
|
||||||
|
cfg.general.ad_tag = new.general.ad_tag.clone();
|
||||||
|
cfg.network.dns_overrides = new.network.dns_overrides.clone();
|
||||||
|
cfg.general.desync_all_full = new.general.desync_all_full;
|
||||||
|
cfg.general.update_every = new.general.update_every;
|
||||||
|
cfg.general.proxy_secret_auto_reload_secs = new.general.proxy_secret_auto_reload_secs;
|
||||||
|
cfg.general.proxy_config_auto_reload_secs = new.general.proxy_config_auto_reload_secs;
|
||||||
|
cfg.general.me_reinit_every_secs = new.general.me_reinit_every_secs;
|
||||||
|
cfg.general.me_reinit_singleflight = new.general.me_reinit_singleflight;
|
||||||
|
cfg.general.me_reinit_coalesce_window_ms = new.general.me_reinit_coalesce_window_ms;
|
||||||
|
cfg.general.hardswap = new.general.hardswap;
|
||||||
|
cfg.general.me_pool_drain_ttl_secs = new.general.me_pool_drain_ttl_secs;
|
||||||
|
cfg.general.me_pool_min_fresh_ratio = new.general.me_pool_min_fresh_ratio;
|
||||||
|
cfg.general.me_reinit_drain_timeout_secs = new.general.me_reinit_drain_timeout_secs;
|
||||||
|
cfg.general.me_hardswap_warmup_delay_min_ms = new.general.me_hardswap_warmup_delay_min_ms;
|
||||||
|
cfg.general.me_hardswap_warmup_delay_max_ms = new.general.me_hardswap_warmup_delay_max_ms;
|
||||||
|
cfg.general.me_hardswap_warmup_extra_passes = new.general.me_hardswap_warmup_extra_passes;
|
||||||
|
cfg.general.me_hardswap_warmup_pass_backoff_base_ms =
|
||||||
|
new.general.me_hardswap_warmup_pass_backoff_base_ms;
|
||||||
|
cfg.general.me_bind_stale_mode = new.general.me_bind_stale_mode;
|
||||||
|
cfg.general.me_bind_stale_ttl_secs = new.general.me_bind_stale_ttl_secs;
|
||||||
|
cfg.general.me_secret_atomic_snapshot = new.general.me_secret_atomic_snapshot;
|
||||||
|
cfg.general.me_deterministic_writer_sort = new.general.me_deterministic_writer_sort;
|
||||||
|
cfg.general.me_single_endpoint_shadow_writers = new.general.me_single_endpoint_shadow_writers;
|
||||||
|
cfg.general.me_single_endpoint_outage_mode_enabled =
|
||||||
|
new.general.me_single_endpoint_outage_mode_enabled;
|
||||||
|
cfg.general.me_single_endpoint_outage_disable_quarantine =
|
||||||
|
new.general.me_single_endpoint_outage_disable_quarantine;
|
||||||
|
cfg.general.me_single_endpoint_outage_backoff_min_ms =
|
||||||
|
new.general.me_single_endpoint_outage_backoff_min_ms;
|
||||||
|
cfg.general.me_single_endpoint_outage_backoff_max_ms =
|
||||||
|
new.general.me_single_endpoint_outage_backoff_max_ms;
|
||||||
|
cfg.general.me_single_endpoint_shadow_rotate_every_secs =
|
||||||
|
new.general.me_single_endpoint_shadow_rotate_every_secs;
|
||||||
|
cfg.general.me_config_stable_snapshots = new.general.me_config_stable_snapshots;
|
||||||
|
cfg.general.me_config_apply_cooldown_secs = new.general.me_config_apply_cooldown_secs;
|
||||||
|
cfg.general.me_snapshot_require_http_2xx = new.general.me_snapshot_require_http_2xx;
|
||||||
|
cfg.general.me_snapshot_reject_empty_map = new.general.me_snapshot_reject_empty_map;
|
||||||
|
cfg.general.me_snapshot_min_proxy_for_lines = new.general.me_snapshot_min_proxy_for_lines;
|
||||||
|
cfg.general.proxy_secret_stable_snapshots = new.general.proxy_secret_stable_snapshots;
|
||||||
|
cfg.general.proxy_secret_rotate_runtime = new.general.proxy_secret_rotate_runtime;
|
||||||
|
cfg.general.proxy_secret_len_max = new.general.proxy_secret_len_max;
|
||||||
|
cfg.general.telemetry = new.general.telemetry.clone();
|
||||||
|
cfg.general.me_socks_kdf_policy = new.general.me_socks_kdf_policy;
|
||||||
|
cfg.general.me_floor_mode = new.general.me_floor_mode;
|
||||||
|
cfg.general.me_adaptive_floor_idle_secs = new.general.me_adaptive_floor_idle_secs;
|
||||||
|
cfg.general.me_adaptive_floor_min_writers_single_endpoint =
|
||||||
|
new.general.me_adaptive_floor_min_writers_single_endpoint;
|
||||||
|
cfg.general.me_adaptive_floor_recover_grace_secs =
|
||||||
|
new.general.me_adaptive_floor_recover_grace_secs;
|
||||||
|
cfg.general.me_route_backpressure_base_timeout_ms =
|
||||||
|
new.general.me_route_backpressure_base_timeout_ms;
|
||||||
|
cfg.general.me_route_backpressure_high_timeout_ms =
|
||||||
|
new.general.me_route_backpressure_high_timeout_ms;
|
||||||
|
cfg.general.me_route_backpressure_high_watermark_pct =
|
||||||
|
new.general.me_route_backpressure_high_watermark_pct;
|
||||||
|
|
||||||
|
cfg.access.users = new.access.users.clone();
|
||||||
|
cfg.access.user_ad_tags = new.access.user_ad_tags.clone();
|
||||||
|
cfg.access.user_max_tcp_conns = new.access.user_max_tcp_conns.clone();
|
||||||
|
cfg.access.user_expirations = new.access.user_expirations.clone();
|
||||||
|
cfg.access.user_data_quota = new.access.user_data_quota.clone();
|
||||||
|
cfg.access.user_max_unique_ips = new.access.user_max_unique_ips.clone();
|
||||||
|
cfg.access.user_max_unique_ips_mode = new.access.user_max_unique_ips_mode;
|
||||||
|
cfg.access.user_max_unique_ips_window_secs = new.access.user_max_unique_ips_window_secs;
|
||||||
|
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
|
||||||
/// Warn if any non-hot fields changed (require restart).
|
/// Warn if any non-hot fields changed (require restart).
|
||||||
fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig) {
|
fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: bool) {
|
||||||
|
let mut warned = false;
|
||||||
if old.server.port != new.server.port {
|
if old.server.port != new.server.port {
|
||||||
|
warned = true;
|
||||||
warn!(
|
warn!(
|
||||||
"config reload: server.port changed ({} → {}); restart required",
|
"config reload: server.port changed ({} → {}); restart required",
|
||||||
old.server.port, new.server.port
|
old.server.port, new.server.port
|
||||||
|
|
@ -125,23 +314,81 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig) {
|
||||||
!= new.server.api.minimal_runtime_cache_ttl_ms
|
!= new.server.api.minimal_runtime_cache_ttl_ms
|
||||||
|| old.server.api.read_only != new.server.api.read_only
|
|| old.server.api.read_only != new.server.api.read_only
|
||||||
{
|
{
|
||||||
|
warned = true;
|
||||||
warn!("config reload: server.api changed; restart required");
|
warn!("config reload: server.api changed; restart required");
|
||||||
}
|
}
|
||||||
|
if old.server.proxy_protocol != new.server.proxy_protocol
|
||||||
|
|| !listeners_equal(&old.server.listeners, &new.server.listeners)
|
||||||
|
|| old.server.listen_addr_ipv4 != new.server.listen_addr_ipv4
|
||||||
|
|| old.server.listen_addr_ipv6 != new.server.listen_addr_ipv6
|
||||||
|
|| old.server.listen_tcp != new.server.listen_tcp
|
||||||
|
|| old.server.listen_unix_sock != new.server.listen_unix_sock
|
||||||
|
|| old.server.listen_unix_sock_perm != new.server.listen_unix_sock_perm
|
||||||
|
{
|
||||||
|
warned = true;
|
||||||
|
warn!("config reload: server listener settings changed; restart required");
|
||||||
|
}
|
||||||
|
if old.censorship.tls_domain != new.censorship.tls_domain
|
||||||
|
|| old.censorship.tls_domains != new.censorship.tls_domains
|
||||||
|
|| old.censorship.mask != new.censorship.mask
|
||||||
|
|| old.censorship.mask_host != new.censorship.mask_host
|
||||||
|
|| old.censorship.mask_port != new.censorship.mask_port
|
||||||
|
|| old.censorship.mask_unix_sock != new.censorship.mask_unix_sock
|
||||||
|
|| old.censorship.fake_cert_len != new.censorship.fake_cert_len
|
||||||
|
|| old.censorship.tls_emulation != new.censorship.tls_emulation
|
||||||
|
|| old.censorship.tls_front_dir != new.censorship.tls_front_dir
|
||||||
|
|| old.censorship.server_hello_delay_min_ms != new.censorship.server_hello_delay_min_ms
|
||||||
|
|| old.censorship.server_hello_delay_max_ms != new.censorship.server_hello_delay_max_ms
|
||||||
|
|| old.censorship.tls_new_session_tickets != new.censorship.tls_new_session_tickets
|
||||||
|
|| old.censorship.tls_full_cert_ttl_secs != new.censorship.tls_full_cert_ttl_secs
|
||||||
|
|| old.censorship.alpn_enforce != new.censorship.alpn_enforce
|
||||||
|
|| old.censorship.mask_proxy_protocol != new.censorship.mask_proxy_protocol
|
||||||
|
{
|
||||||
|
warned = true;
|
||||||
|
warn!("config reload: censorship settings changed; restart required");
|
||||||
|
}
|
||||||
if old.censorship.tls_domain != new.censorship.tls_domain {
|
if old.censorship.tls_domain != new.censorship.tls_domain {
|
||||||
|
warned = true;
|
||||||
warn!(
|
warn!(
|
||||||
"config reload: censorship.tls_domain changed ('{}' → '{}'); restart required",
|
"config reload: censorship.tls_domain changed ('{}' → '{}'); restart required",
|
||||||
old.censorship.tls_domain, new.censorship.tls_domain
|
old.censorship.tls_domain, new.censorship.tls_domain
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if old.network.ipv4 != new.network.ipv4 || old.network.ipv6 != new.network.ipv6 {
|
if old.network.ipv4 != new.network.ipv4 || old.network.ipv6 != new.network.ipv6 {
|
||||||
|
warned = true;
|
||||||
warn!("config reload: network.ipv4/ipv6 changed; restart required");
|
warn!("config reload: network.ipv4/ipv6 changed; restart required");
|
||||||
}
|
}
|
||||||
|
if old.network.prefer != new.network.prefer
|
||||||
|
|| old.network.multipath != new.network.multipath
|
||||||
|
|| old.network.stun_use != new.network.stun_use
|
||||||
|
|| old.network.stun_servers != new.network.stun_servers
|
||||||
|
|| old.network.stun_tcp_fallback != new.network.stun_tcp_fallback
|
||||||
|
|| old.network.http_ip_detect_urls != new.network.http_ip_detect_urls
|
||||||
|
|| old.network.cache_public_ip_path != new.network.cache_public_ip_path
|
||||||
|
{
|
||||||
|
warned = true;
|
||||||
|
warn!("config reload: non-hot network settings changed; restart required");
|
||||||
|
}
|
||||||
if old.general.use_middle_proxy != new.general.use_middle_proxy {
|
if old.general.use_middle_proxy != new.general.use_middle_proxy {
|
||||||
|
warned = true;
|
||||||
warn!("config reload: use_middle_proxy changed; restart required");
|
warn!("config reload: use_middle_proxy changed; restart required");
|
||||||
}
|
}
|
||||||
if old.general.stun_nat_probe_concurrency != new.general.stun_nat_probe_concurrency {
|
if old.general.stun_nat_probe_concurrency != new.general.stun_nat_probe_concurrency {
|
||||||
|
warned = true;
|
||||||
warn!("config reload: general.stun_nat_probe_concurrency changed; restart required");
|
warn!("config reload: general.stun_nat_probe_concurrency changed; restart required");
|
||||||
}
|
}
|
||||||
|
if old.general.middle_proxy_pool_size != new.general.middle_proxy_pool_size {
|
||||||
|
warned = true;
|
||||||
|
warn!("config reload: general.middle_proxy_pool_size changed; restart required");
|
||||||
|
}
|
||||||
|
if old.general.me_keepalive_enabled != new.general.me_keepalive_enabled
|
||||||
|
|| old.general.me_keepalive_interval_secs != new.general.me_keepalive_interval_secs
|
||||||
|
|| old.general.me_keepalive_jitter_secs != new.general.me_keepalive_jitter_secs
|
||||||
|
|| old.general.me_keepalive_payload_random != new.general.me_keepalive_payload_random
|
||||||
|
{
|
||||||
|
warned = true;
|
||||||
|
warn!("config reload: general.me_keepalive_* changed; restart required");
|
||||||
|
}
|
||||||
if old.general.upstream_connect_retry_attempts != new.general.upstream_connect_retry_attempts
|
if old.general.upstream_connect_retry_attempts != new.general.upstream_connect_retry_attempts
|
||||||
|| old.general.upstream_connect_retry_backoff_ms
|
|| old.general.upstream_connect_retry_backoff_ms
|
||||||
!= new.general.upstream_connect_retry_backoff_ms
|
!= new.general.upstream_connect_retry_backoff_ms
|
||||||
|
|
@ -151,8 +398,12 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig) {
|
||||||
!= new.general.upstream_connect_failfast_hard_errors
|
!= new.general.upstream_connect_failfast_hard_errors
|
||||||
|| old.general.rpc_proxy_req_every != new.general.rpc_proxy_req_every
|
|| old.general.rpc_proxy_req_every != new.general.rpc_proxy_req_every
|
||||||
{
|
{
|
||||||
|
warned = true;
|
||||||
warn!("config reload: general.upstream_* changed; restart required");
|
warn!("config reload: general.upstream_* changed; restart required");
|
||||||
}
|
}
|
||||||
|
if non_hot_changed && !warned {
|
||||||
|
warn!("config reload: one or more non-hot fields changed; restart required");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the public host for link generation — mirrors the logic in main.rs.
|
/// Resolve the public host for link generation — mirrors the logic in main.rs.
|
||||||
|
|
@ -235,10 +486,10 @@ fn log_changes(
|
||||||
log_tx.send(new_hot.log_level.clone()).ok();
|
log_tx.send(new_hot.log_level.clone()).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_hot.access.user_ad_tags != new_hot.access.user_ad_tags {
|
if old_hot.user_ad_tags != new_hot.user_ad_tags {
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_ad_tags updated ({} entries)",
|
"config reload: user_ad_tags updated ({} entries)",
|
||||||
new_hot.access.user_ad_tags.len(),
|
new_hot.user_ad_tags.len(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,13 +504,6 @@ fn log_changes(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_hot.middle_proxy_pool_size != new_hot.middle_proxy_pool_size {
|
|
||||||
info!(
|
|
||||||
"config reload: middle_proxy_pool_size: {} → {}",
|
|
||||||
old_hot.middle_proxy_pool_size, new_hot.middle_proxy_pool_size,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if old_hot.desync_all_full != new_hot.desync_all_full {
|
if old_hot.desync_all_full != new_hot.desync_all_full {
|
||||||
info!(
|
info!(
|
||||||
"config reload: desync_all_full: {} → {}",
|
"config reload: desync_all_full: {} → {}",
|
||||||
|
|
@ -273,6 +517,17 @@ fn log_changes(
|
||||||
old_hot.update_every_secs, new_hot.update_every_secs,
|
old_hot.update_every_secs, new_hot.update_every_secs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if old_hot.me_reinit_every_secs != new_hot.me_reinit_every_secs
|
||||||
|
|| old_hot.me_reinit_singleflight != new_hot.me_reinit_singleflight
|
||||||
|
|| old_hot.me_reinit_coalesce_window_ms != new_hot.me_reinit_coalesce_window_ms
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"config reload: me_reinit: interval={}s singleflight={} coalesce={}ms",
|
||||||
|
new_hot.me_reinit_every_secs,
|
||||||
|
new_hot.me_reinit_singleflight,
|
||||||
|
new_hot.me_reinit_coalesce_window_ms
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if old_hot.hardswap != new_hot.hardswap {
|
if old_hot.hardswap != new_hot.hardswap {
|
||||||
info!(
|
info!(
|
||||||
|
|
@ -301,18 +556,84 @@ fn log_changes(
|
||||||
old_hot.me_reinit_drain_timeout_secs, new_hot.me_reinit_drain_timeout_secs,
|
old_hot.me_reinit_drain_timeout_secs, new_hot.me_reinit_drain_timeout_secs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if old_hot.me_hardswap_warmup_delay_min_ms != new_hot.me_hardswap_warmup_delay_min_ms
|
||||||
if old_hot.me_keepalive_enabled != new_hot.me_keepalive_enabled
|
|| old_hot.me_hardswap_warmup_delay_max_ms != new_hot.me_hardswap_warmup_delay_max_ms
|
||||||
|| old_hot.me_keepalive_interval_secs != new_hot.me_keepalive_interval_secs
|
|| old_hot.me_hardswap_warmup_extra_passes != new_hot.me_hardswap_warmup_extra_passes
|
||||||
|| old_hot.me_keepalive_jitter_secs != new_hot.me_keepalive_jitter_secs
|
|| old_hot.me_hardswap_warmup_pass_backoff_base_ms
|
||||||
|| old_hot.me_keepalive_payload_random != new_hot.me_keepalive_payload_random
|
!= new_hot.me_hardswap_warmup_pass_backoff_base_ms
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"config reload: me_keepalive: enabled={} interval={}s jitter={}s random_payload={}",
|
"config reload: me_hardswap_warmup: min={}ms max={}ms extra_passes={} pass_backoff={}ms",
|
||||||
new_hot.me_keepalive_enabled,
|
new_hot.me_hardswap_warmup_delay_min_ms,
|
||||||
new_hot.me_keepalive_interval_secs,
|
new_hot.me_hardswap_warmup_delay_max_ms,
|
||||||
new_hot.me_keepalive_jitter_secs,
|
new_hot.me_hardswap_warmup_extra_passes,
|
||||||
new_hot.me_keepalive_payload_random,
|
new_hot.me_hardswap_warmup_pass_backoff_base_ms
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if old_hot.me_bind_stale_mode != new_hot.me_bind_stale_mode
|
||||||
|
|| old_hot.me_bind_stale_ttl_secs != new_hot.me_bind_stale_ttl_secs
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"config reload: me_bind_stale: mode={:?} ttl={}s",
|
||||||
|
new_hot.me_bind_stale_mode,
|
||||||
|
new_hot.me_bind_stale_ttl_secs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if old_hot.me_secret_atomic_snapshot != new_hot.me_secret_atomic_snapshot
|
||||||
|
|| old_hot.me_deterministic_writer_sort != new_hot.me_deterministic_writer_sort
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"config reload: me_runtime_flags: secret_atomic_snapshot={} deterministic_sort={}",
|
||||||
|
new_hot.me_secret_atomic_snapshot,
|
||||||
|
new_hot.me_deterministic_writer_sort
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if old_hot.me_single_endpoint_shadow_writers != new_hot.me_single_endpoint_shadow_writers
|
||||||
|
|| old_hot.me_single_endpoint_outage_mode_enabled
|
||||||
|
!= new_hot.me_single_endpoint_outage_mode_enabled
|
||||||
|
|| old_hot.me_single_endpoint_outage_disable_quarantine
|
||||||
|
!= new_hot.me_single_endpoint_outage_disable_quarantine
|
||||||
|
|| old_hot.me_single_endpoint_outage_backoff_min_ms
|
||||||
|
!= new_hot.me_single_endpoint_outage_backoff_min_ms
|
||||||
|
|| old_hot.me_single_endpoint_outage_backoff_max_ms
|
||||||
|
!= new_hot.me_single_endpoint_outage_backoff_max_ms
|
||||||
|
|| old_hot.me_single_endpoint_shadow_rotate_every_secs
|
||||||
|
!= new_hot.me_single_endpoint_shadow_rotate_every_secs
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"config reload: me_single_endpoint: shadow={} outage_enabled={} disable_quarantine={} backoff=[{}..{}]ms rotate={}s",
|
||||||
|
new_hot.me_single_endpoint_shadow_writers,
|
||||||
|
new_hot.me_single_endpoint_outage_mode_enabled,
|
||||||
|
new_hot.me_single_endpoint_outage_disable_quarantine,
|
||||||
|
new_hot.me_single_endpoint_outage_backoff_min_ms,
|
||||||
|
new_hot.me_single_endpoint_outage_backoff_max_ms,
|
||||||
|
new_hot.me_single_endpoint_shadow_rotate_every_secs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if old_hot.me_config_stable_snapshots != new_hot.me_config_stable_snapshots
|
||||||
|
|| old_hot.me_config_apply_cooldown_secs != new_hot.me_config_apply_cooldown_secs
|
||||||
|
|| old_hot.me_snapshot_require_http_2xx != new_hot.me_snapshot_require_http_2xx
|
||||||
|
|| old_hot.me_snapshot_reject_empty_map != new_hot.me_snapshot_reject_empty_map
|
||||||
|
|| old_hot.me_snapshot_min_proxy_for_lines != new_hot.me_snapshot_min_proxy_for_lines
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"config reload: me_snapshot_guard: stable={} cooldown={}s require_2xx={} reject_empty={} min_proxy_for={}",
|
||||||
|
new_hot.me_config_stable_snapshots,
|
||||||
|
new_hot.me_config_apply_cooldown_secs,
|
||||||
|
new_hot.me_snapshot_require_http_2xx,
|
||||||
|
new_hot.me_snapshot_reject_empty_map,
|
||||||
|
new_hot.me_snapshot_min_proxy_for_lines
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if old_hot.proxy_secret_stable_snapshots != new_hot.proxy_secret_stable_snapshots
|
||||||
|
|| old_hot.proxy_secret_rotate_runtime != new_hot.proxy_secret_rotate_runtime
|
||||||
|
|| old_hot.proxy_secret_len_max != new_hot.proxy_secret_len_max
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"config reload: proxy_secret_runtime: stable={} rotate={} len_max={}",
|
||||||
|
new_hot.proxy_secret_stable_snapshots,
|
||||||
|
new_hot.proxy_secret_rotate_runtime,
|
||||||
|
new_hot.proxy_secret_len_max
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,21 +688,21 @@ fn log_changes(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_hot.access.users != new_hot.access.users {
|
if old_hot.users != new_hot.users {
|
||||||
let mut added: Vec<&String> = new_hot.access.users.keys()
|
let mut added: Vec<&String> = new_hot.users.keys()
|
||||||
.filter(|u| !old_hot.access.users.contains_key(*u))
|
.filter(|u| !old_hot.users.contains_key(*u))
|
||||||
.collect();
|
.collect();
|
||||||
added.sort();
|
added.sort();
|
||||||
|
|
||||||
let mut removed: Vec<&String> = old_hot.access.users.keys()
|
let mut removed: Vec<&String> = old_hot.users.keys()
|
||||||
.filter(|u| !new_hot.access.users.contains_key(*u))
|
.filter(|u| !new_hot.users.contains_key(*u))
|
||||||
.collect();
|
.collect();
|
||||||
removed.sort();
|
removed.sort();
|
||||||
|
|
||||||
let mut changed: Vec<&String> = new_hot.access.users.keys()
|
let mut changed: Vec<&String> = new_hot.users.keys()
|
||||||
.filter(|u| {
|
.filter(|u| {
|
||||||
old_hot.access.users.get(*u)
|
old_hot.users.get(*u)
|
||||||
.map(|s| s != &new_hot.access.users[*u])
|
.map(|s| s != &new_hot.users[*u])
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -395,7 +716,7 @@ fn log_changes(
|
||||||
let host = resolve_link_host(new_cfg, detected_ip_v4, detected_ip_v6);
|
let host = resolve_link_host(new_cfg, detected_ip_v4, detected_ip_v6);
|
||||||
let port = new_cfg.general.links.public_port.unwrap_or(new_cfg.server.port);
|
let port = new_cfg.general.links.public_port.unwrap_or(new_cfg.server.port);
|
||||||
for user in &added {
|
for user in &added {
|
||||||
if let Some(secret) = new_hot.access.users.get(*user) {
|
if let Some(secret) = new_hot.users.get(*user) {
|
||||||
print_user_links(user, secret, &host, port, new_cfg);
|
print_user_links(user, secret, &host, port, new_cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -414,38 +735,38 @@ fn log_changes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_hot.access.user_max_tcp_conns != new_hot.access.user_max_tcp_conns {
|
if old_hot.user_max_tcp_conns != new_hot.user_max_tcp_conns {
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_max_tcp_conns updated ({} entries)",
|
"config reload: user_max_tcp_conns updated ({} entries)",
|
||||||
new_hot.access.user_max_tcp_conns.len()
|
new_hot.user_max_tcp_conns.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if old_hot.access.user_expirations != new_hot.access.user_expirations {
|
if old_hot.user_expirations != new_hot.user_expirations {
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_expirations updated ({} entries)",
|
"config reload: user_expirations updated ({} entries)",
|
||||||
new_hot.access.user_expirations.len()
|
new_hot.user_expirations.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if old_hot.access.user_data_quota != new_hot.access.user_data_quota {
|
if old_hot.user_data_quota != new_hot.user_data_quota {
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_data_quota updated ({} entries)",
|
"config reload: user_data_quota updated ({} entries)",
|
||||||
new_hot.access.user_data_quota.len()
|
new_hot.user_data_quota.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if old_hot.access.user_max_unique_ips != new_hot.access.user_max_unique_ips {
|
if old_hot.user_max_unique_ips != new_hot.user_max_unique_ips {
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_max_unique_ips updated ({} entries)",
|
"config reload: user_max_unique_ips updated ({} entries)",
|
||||||
new_hot.access.user_max_unique_ips.len()
|
new_hot.user_max_unique_ips.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if old_hot.access.user_max_unique_ips_mode != new_hot.access.user_max_unique_ips_mode
|
if old_hot.user_max_unique_ips_mode != new_hot.user_max_unique_ips_mode
|
||||||
|| old_hot.access.user_max_unique_ips_window_secs
|
|| old_hot.user_max_unique_ips_window_secs
|
||||||
!= new_hot.access.user_max_unique_ips_window_secs
|
!= new_hot.user_max_unique_ips_window_secs
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_max_unique_ips policy mode={:?} window={}s",
|
"config reload: user_max_unique_ips policy mode={:?} window={}s",
|
||||||
new_hot.access.user_max_unique_ips_mode,
|
new_hot.user_max_unique_ips_mode,
|
||||||
new_hot.access.user_max_unique_ips_window_secs
|
new_hot.user_max_unique_ips_window_secs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -472,15 +793,22 @@ fn reload_config(
|
||||||
}
|
}
|
||||||
|
|
||||||
let old_cfg = config_tx.borrow().clone();
|
let old_cfg = config_tx.borrow().clone();
|
||||||
|
let applied_cfg = overlay_hot_fields(&old_cfg, &new_cfg);
|
||||||
let old_hot = HotFields::from_config(&old_cfg);
|
let old_hot = HotFields::from_config(&old_cfg);
|
||||||
let new_hot = HotFields::from_config(&new_cfg);
|
let applied_hot = HotFields::from_config(&applied_cfg);
|
||||||
|
let non_hot_changed = !config_equal(&applied_cfg, &new_cfg);
|
||||||
|
let hot_changed = old_hot != applied_hot;
|
||||||
|
|
||||||
if old_hot == new_hot {
|
if non_hot_changed {
|
||||||
|
warn_non_hot_changes(&old_cfg, &new_cfg, non_hot_changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hot_changed {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_hot.dns_overrides != new_hot.dns_overrides
|
if old_hot.dns_overrides != applied_hot.dns_overrides
|
||||||
&& let Err(e) = crate::network::dns_overrides::install_entries(&new_hot.dns_overrides)
|
&& let Err(e) = crate::network::dns_overrides::install_entries(&applied_hot.dns_overrides)
|
||||||
{
|
{
|
||||||
error!(
|
error!(
|
||||||
"config reload: invalid network.dns_overrides: {}; keeping old config",
|
"config reload: invalid network.dns_overrides: {}; keeping old config",
|
||||||
|
|
@ -489,9 +817,15 @@ fn reload_config(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
warn_non_hot_changes(&old_cfg, &new_cfg);
|
log_changes(
|
||||||
log_changes(&old_hot, &new_hot, &new_cfg, log_tx, detected_ip_v4, detected_ip_v6);
|
&old_hot,
|
||||||
config_tx.send(Arc::new(new_cfg)).ok();
|
&applied_hot,
|
||||||
|
&applied_cfg,
|
||||||
|
log_tx,
|
||||||
|
detected_ip_v4,
|
||||||
|
detected_ip_v6,
|
||||||
|
);
|
||||||
|
config_tx.send(Arc::new(applied_cfg)).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────────────────────
|
// ── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -617,3 +951,80 @@ pub fn spawn_config_watcher(
|
||||||
|
|
||||||
(config_rx, log_rx)
|
(config_rx, log_rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn sample_config() -> ProxyConfig {
|
||||||
|
ProxyConfig::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overlay_applies_hot_and_preserves_non_hot() {
|
||||||
|
let old = sample_config();
|
||||||
|
let mut new = old.clone();
|
||||||
|
new.general.hardswap = !old.general.hardswap;
|
||||||
|
new.server.port = old.server.port.saturating_add(1);
|
||||||
|
|
||||||
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
|
assert_eq!(applied.general.hardswap, new.general.hardswap);
|
||||||
|
assert_eq!(applied.server.port, old.server.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_hot_only_change_does_not_change_hot_snapshot() {
|
||||||
|
let old = sample_config();
|
||||||
|
let mut new = old.clone();
|
||||||
|
new.server.port = old.server.port.saturating_add(1);
|
||||||
|
|
||||||
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
|
assert_eq!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
||||||
|
assert_eq!(applied.server.port, old.server.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bind_stale_mode_is_hot() {
|
||||||
|
let old = sample_config();
|
||||||
|
let mut new = old.clone();
|
||||||
|
new.general.me_bind_stale_mode = match old.general.me_bind_stale_mode {
|
||||||
|
MeBindStaleMode::Never => MeBindStaleMode::Ttl,
|
||||||
|
MeBindStaleMode::Ttl => MeBindStaleMode::Always,
|
||||||
|
MeBindStaleMode::Always => MeBindStaleMode::Never,
|
||||||
|
};
|
||||||
|
|
||||||
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
|
assert_eq!(
|
||||||
|
applied.general.me_bind_stale_mode,
|
||||||
|
new.general.me_bind_stale_mode
|
||||||
|
);
|
||||||
|
assert_ne!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keepalive_is_not_hot() {
|
||||||
|
let old = sample_config();
|
||||||
|
let mut new = old.clone();
|
||||||
|
new.general.me_keepalive_interval_secs = old.general.me_keepalive_interval_secs + 5;
|
||||||
|
|
||||||
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
|
assert_eq!(
|
||||||
|
applied.general.me_keepalive_interval_secs,
|
||||||
|
old.general.me_keepalive_interval_secs
|
||||||
|
);
|
||||||
|
assert_eq!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_hot_and_non_hot_change_applies_only_hot_subset() {
|
||||||
|
let old = sample_config();
|
||||||
|
let mut new = old.clone();
|
||||||
|
new.general.hardswap = !old.general.hardswap;
|
||||||
|
new.general.use_middle_proxy = !old.general.use_middle_proxy;
|
||||||
|
|
||||||
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
|
assert_eq!(applied.general.hardswap, new.general.hardswap);
|
||||||
|
assert_eq!(applied.general.use_middle_proxy, old.general.use_middle_proxy);
|
||||||
|
assert!(!config_equal(&applied, &new));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue