WEB: Resolve opt-in decoy hostnames during config preparation #929

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-10-04 14:32:06 +03:00
parent d3de9865cf
commit bc1c130f38
30 changed files with 1251 additions and 168 deletions
+43 -42
View File
@@ -1,5 +1,8 @@
use super::*;
// Source polling and explicit reloads have distinct DNS preparation lifecycles.
mod preparation;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct WatchManifest {
files: BTreeSet<PathBuf>,
@@ -132,9 +135,21 @@ fn apply_watch_manifest<W1: Watcher, W2: Watcher>(
}
}
/// Load config, validate, diff against current, and broadcast if changed.
/// Builds the effective WEB snapshot without changing live state.
fn prepare_effective_config(
old_cfg: &ProxyConfig,
new_cfg: &ProxyConfig,
) -> crate::error::Result<ProxyConfig> {
new_cfg.validate()?;
let mut applied_cfg = overlay_hot_fields(old_cfg, new_cfg);
applied_cfg.validate_effective_web()?;
applied_cfg.rebuild_runtime_web()?;
Ok(applied_cfg)
}
/// Diffs and publishes a fully prepared configuration on the watcher owner task.
fn reload_config_with_resolver(
config_path: &PathBuf,
prepared: (LoadedConfig, ProxyConfig),
config_tx: &watch::Sender<Arc<ProxyConfig>>,
log_tx: &watch::Sender<LogLevel>,
detected_ip_v4: Option<IpAddr>,
@@ -142,13 +157,7 @@ fn reload_config_with_resolver(
reload_state: &mut ReloadState,
dns_resolver: Option<&crate::network::dns_overrides::GenerationDnsResolver>,
) -> Option<WatchManifest> {
let loaded = match ProxyConfig::load_with_metadata(config_path) {
Ok(loaded) => loaded,
Err(e) => {
error!("config reload: failed to parse {:?}: {}", config_path, e);
return None;
}
};
let (loaded, applied_cfg) = prepared;
let LoadedConfig {
config: new_cfg,
source_files,
@@ -157,34 +166,12 @@ fn reload_config_with_resolver(
} = loaded;
let next_manifest = WatchManifest::from_source_files(&source_files);
if let Err(e) = new_cfg.validate() {
error!(
"config reload: validation failed: {}; keeping old config",
e
);
return Some(next_manifest);
}
if reload_state.is_applied(rendered_hash) {
return Some(next_manifest);
}
let old_cfg = config_tx.borrow().clone();
let mut applied_cfg = overlay_hot_fields(&old_cfg, &new_cfg);
if let Err(error) = applied_cfg
.validate_effective_web()
.and_then(|_| applied_cfg.rebuild_runtime_web())
{
error!(
"config reload: effective WEB validation failed: {}; keeping old config",
error
);
return Some(next_manifest);
}
let old_hot = HotFields::from_config(&old_cfg);
let applied_hot = HotFields::from_config(&applied_cfg);
let non_hot_changed = !config_equal(&applied_cfg, &new_cfg);
let hot_changed = !config_equal(&old_cfg, &applied_cfg);
let hot_changed =
!config_equal(&old_cfg, &applied_cfg) || !old_cfg.web_decoy_endpoints_equal(&applied_cfg);
if non_hot_changed {
warn_non_hot_changes(&old_cfg, &new_cfg, non_hot_changed);
@@ -228,8 +215,13 @@ pub(super) fn reload_config(
detected_ip_v6: Option<IpAddr>,
reload_state: &mut ReloadState,
) -> Option<WatchManifest> {
let loaded = ProxyConfig::load_with_metadata(config_path).ok()?;
if reload_state.is_applied(loaded.rendered_hash) {
return Some(WatchManifest::from_source_files(&loaded.source_files));
}
let applied = prepare_effective_config(&config_tx.borrow(), &loaded.config).ok()?;
reload_config_with_resolver(
config_path,
(loaded, applied),
config_tx,
log_tx,
detected_ip_v4,
@@ -282,7 +274,7 @@ pub fn spawn_config_watcher(
}
}
}
let initial_loaded = ProxyConfig::load_with_metadata(&config_path).ok();
let initial_loaded = preparation::read_source(&config_path).await;
let initial_manifest = initial_loaded
.as_ref()
.map(|loaded| WatchManifest::from_source_files(&loaded.source_files))
@@ -291,7 +283,7 @@ pub fn spawn_config_watcher(
});
let initial_matches_disk = initial_loaded
.as_ref()
.is_some_and(|loaded| config_equal(config_tx.borrow().as_ref(), &loaded.config));
.is_some_and(|source| preparation::source_matches_active(&config_tx.borrow(), source));
let initial_snapshot_hash = initial_loaded
.as_ref()
.filter(|_| initial_matches_disk)
@@ -384,12 +376,14 @@ pub fn spawn_config_watcher(
};
loop {
let mut force = false;
#[cfg(unix)]
tokio::select! {
msg = notify_rx.recv() => {
if msg.is_none() { break; }
}
_ = sighup.recv() => {
force = true;
info!("SIGHUP received — reloading {:?}", config_path);
}
_ = cancellation.cancelled() => break,
@@ -406,27 +400,31 @@ pub fn spawn_config_watcher(
tokio::time::sleep(HOT_RELOAD_DEBOUNCE).await;
while notify_rx.try_recv().is_ok() {}
let mut next_manifest = reload_config_with_resolver(
let mut next_manifest = preparation::reload(
&config_path,
&config_tx,
&log_tx,
detected_ip_v4,
detected_ip_v6,
&mut reload_state,
dns_resolver.as_deref(),
);
dns_resolver.clone(),
force,
)
.await;
if next_manifest.is_none() {
tokio::time::sleep(HOT_RELOAD_DEBOUNCE).await;
while notify_rx.try_recv().is_ok() {}
next_manifest = reload_config_with_resolver(
next_manifest = preparation::reload(
&config_path,
&config_tx,
&log_tx,
detected_ip_v4,
detected_ip_v6,
&mut reload_state,
dns_resolver.as_deref(),
);
dns_resolver.clone(),
force,
)
.await;
}
if let Some(next_manifest) = next_manifest {
@@ -460,3 +458,6 @@ mod path_tests {
assert_eq!(normalize_watch_path(&linked), linked);
}
}
#[cfg(test)]
mod decoy_dns_tests;