This commit is contained in:
Alexey
2026-09-25 19:46:31 +03:00
parent 2d63fcf376
commit 326c0ecdb9
128 changed files with 914 additions and 1248 deletions
+2 -2
View File
@@ -6,14 +6,14 @@ use serde_json::Value as Json;
use toml::Value as Toml;
use super::ApiShared;
#[cfg(test)]
use super::config_store::write_atomic;
use super::config_store::{
EDITABLE_SECTIONS, EDITABLE_SERVER_FIELDS, compute_snapshot_revision, is_editable_section,
load_candidate_snapshot, load_config_snapshot, render_server_listeners,
render_top_level_section, resolve_single_source_owner, upsert_toml_table,
write_atomic_if_unchanged,
};
#[cfg(test)]
use super::config_store::write_atomic;
use super::model::ApiFailure;
use crate::config::ProxyConfig;
use crate::config::hot_reload::classify_config_changes;
+1 -1
View File
@@ -13,13 +13,13 @@ mod persistence;
// Compare-and-replace file persistence and metadata preservation.
mod atomic;
pub(in crate::api) use atomic::{write_atomic, write_atomic_if_unchanged};
#[cfg(test)]
use persistence::{find_toml_table_bounds, render_access_section, save_sections_to_disk};
pub(in crate::api) use persistence::{
render_server_listeners, render_top_level_section, save_access_sections_to_disk,
save_access_sections_to_disk_if_revision, upsert_toml_table,
};
pub(in crate::api) use atomic::{write_atomic, write_atomic_if_unchanged};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum AccessSection {
+8 -15
View File
@@ -86,9 +86,9 @@ pub(in crate::api) async fn write_atomic(
let _lock = ConfigWriteLock::acquire(&path)?;
write_atomic_sync(&path, None, &contents, None).map(|_| ())
})
.await
.map_err(|error| ApiFailure::internal(format!("failed to join writer: {error}")))?
.map_err(|error| ApiFailure::internal(format!("failed to write config: {error}")))
.await
.map_err(|error| ApiFailure::internal(format!("failed to join writer: {error}")))?
.map_err(|error| ApiFailure::internal(format!("failed to write config: {error}")))
}
/// Replaces one source only if both its graph revision and owner contents are unchanged.
@@ -294,11 +294,7 @@ fn write_atomic_sync(
let descriptor = openat(
anchored.parent(),
temp_name.as_str(),
OFlag::O_WRONLY
| OFlag::O_CREAT
| OFlag::O_EXCL
| OFlag::O_NOFOLLOW
| OFlag::O_CLOEXEC,
OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC,
Mode::from_bits_truncate(0o600),
)
.map_err(errno_to_io)?;
@@ -408,9 +404,9 @@ fn validate_expected_contents(
existing: Option<&ExistingTarget>,
expected_contents: Option<&str>,
) -> std::io::Result<()> {
if expected_contents.is_some_and(|expected| {
existing.is_none_or(|target| target.contents != expected)
}) {
if expected_contents
.is_some_and(|expected| existing.is_none_or(|target| target.contents != expected))
{
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"config source changed before persistence",
@@ -419,10 +415,7 @@ fn validate_expected_contents(
Ok(())
}
fn target_unchanged(
existing: Option<&ExistingTarget>,
current: Option<&ExistingTarget>,
) -> bool {
fn target_unchanged(existing: Option<&ExistingTarget>, current: Option<&ExistingTarget>) -> bool {
match (existing, current) {
(Some(expected), Some(current)) => {
same_target(&expected.metadata, &current.metadata)
+3 -3
View File
@@ -6,15 +6,15 @@ use serde::Serialize;
use crate::config::{ProxyConfig, RateLimitBps};
#[cfg(test)]
use super::atomic::write_atomic;
use super::atomic::write_atomic_if_unchanged;
#[cfg(test)]
use super::compute_revision;
use super::{
AccessSection, compute_snapshot_revision, load_candidate_snapshot, load_config_snapshot,
resolve_single_source_owner, toml_path_exists,
};
use super::atomic::write_atomic_if_unchanged;
#[cfg(test)]
use super::atomic::write_atomic;
use crate::api::model::ApiFailure;
/// Re-render the given top-level tables from `cfg` and upsert each into the
+5 -3
View File
@@ -266,8 +266,7 @@ async fn access_mutation_rejects_source_graph_change_after_snapshot() {
let root = dir.path().join("config.toml");
let included = dir.path().join("users.toml");
let root_body = "include = \"users.toml\"\n[censorship]\ntls_domain = \"one.example\"\n";
let external_root =
"include = \"users.toml\"\n[censorship]\ntls_domain = \"two.example\"\n";
let external_root = "include = \"users.toml\"\n[censorship]\ntls_domain = \"two.example\"\n";
let included_body = "[access.users]\nalice = \"00000000000000000000000000000000\"\n";
tokio::fs::write(&root, root_body).await.unwrap();
tokio::fs::write(&included, included_body).await.unwrap();
@@ -288,7 +287,10 @@ async fn access_mutation_rejects_source_graph_change_after_snapshot() {
.unwrap_err();
assert_eq!(error.code, "revision_conflict");
assert_eq!(tokio::fs::read_to_string(&root).await.unwrap(), external_root);
assert_eq!(
tokio::fs::read_to_string(&root).await.unwrap(),
external_root
);
assert_eq!(
tokio::fs::read_to_string(&included).await.unwrap(),
included_body
+6 -8
View File
@@ -103,10 +103,9 @@ pub(super) async fn handle(
};
let runtime_cfg = config_rx.borrow().clone();
data.in_runtime = runtime_cfg.access.users.contains_key(&data.username);
shared.runtime_events.record(
"api.user.disable.ok",
format!("username={}", base_user),
);
shared
.runtime_events
.record("api.user.disable.ok", format!("username={}", base_user));
let status = if data.in_runtime {
StatusCode::OK
} else {
@@ -328,10 +327,9 @@ pub(super) async fn handle(
return Err(error);
}
};
shared.runtime_events.record(
"api.user.delete.ok",
format!("username={}", deleted_user),
);
shared
.runtime_events
.record("api.user.delete.ok", format!("username={}", deleted_user));
let runtime_cfg = config_rx.borrow().clone();
let in_runtime = runtime_cfg.access.users.contains_key(&deleted_user);
let response = DeleteUserResponse {
+1 -3
View File
@@ -314,9 +314,7 @@ async fn recompute_connections_payload(
let mut active_users = 0usize;
for entry in shared.stats.iter_user_stats() {
let user_stats = entry.value();
let current_connections = shared
.stats
.get_process_user_curr_connects(entry.key());
let current_connections = shared.stats.get_process_user_curr_connects(entry.key());
let total_octets = user_stats
.octets_from_client
.load(std::sync::atomic::Ordering::Relaxed)
+2 -4
View File
@@ -4,7 +4,7 @@ use std::collections::BTreeSet;
use serde::Serialize;
use super::{now_epoch_secs, ApiShared, SOURCE_UNAVAILABLE_REASON};
use super::{ApiShared, SOURCE_UNAVAILABLE_REASON, now_epoch_secs};
#[derive(Serialize)]
struct RuntimeMePoolStateGenerationData {
@@ -90,9 +90,7 @@ struct RuntimeMePoolStateData {
}
/// Builds the bounded runtime ME pool response projection.
pub(in crate::api) async fn build_runtime_me_pool_state_data(
shared: &ApiShared,
) -> impl Serialize {
pub(in crate::api) async fn build_runtime_me_pool_state_data(shared: &ApiShared) -> impl Serialize {
let now_epoch_secs = now_epoch_secs();
let Some(pool) = shared.me_pool.read().await.clone() else {
return RuntimeMePoolStateData {
+2 -4
View File
@@ -63,12 +63,10 @@ pub(super) fn build_zero_all_data(stats: &Stats, configured_users: usize) -> Zer
conntrack_rule_apply_ok: stats.get_conntrack_rule_apply_ok(),
conntrack_rule_reconcile_success_total: stats
.get_conntrack_rule_reconcile_success_total(),
conntrack_rule_reconcile_error_total: stats
.get_conntrack_rule_reconcile_error_total(),
conntrack_rule_reconcile_error_total: stats.get_conntrack_rule_reconcile_error_total(),
conntrack_rule_rollback_success_total: stats
.get_conntrack_rule_rollback_success_total(),
conntrack_rule_rollback_error_total: stats
.get_conntrack_rule_rollback_error_total(),
conntrack_rule_rollback_error_total: stats.get_conntrack_rule_rollback_error_total(),
conntrack_delete_attempt_total: stats.get_conntrack_delete_attempt_total(),
conntrack_delete_success_total: stats.get_conntrack_delete_success_total(),
conntrack_delete_not_found_total: stats.get_conntrack_delete_not_found_total(),
+5 -7
View File
@@ -145,13 +145,11 @@ async fn create_user_to_completion(
Some(&base_revision),
)
.await?;
shared
.proxy_shared
.stage_user_credential(
&body.username,
credential_id,
cfg.access.is_user_enabled(&body.username),
);
shared.proxy_shared.stage_user_credential(
&body.username,
credential_id,
cfg.access.is_user_enabled(&body.username),
);
if let Some(limit) = updated_limit {
shared
+5 -3
View File
@@ -54,9 +54,11 @@ async fn rotate_secret_to_completion(
Some(&base_revision),
)
.await?;
shared
.proxy_shared
.stage_user_credential(user, credential_id, cfg.access.is_user_enabled(user));
shared.proxy_shared.stage_user_credential(
user,
credential_id,
cfg.access.is_user_enabled(user),
);
drop(_guard);
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
+18 -16
View File
@@ -154,19 +154,19 @@ async fn patch_user_to_completion(
cfg.validate()
.map_err(|e| ApiFailure::bad_request(format!("config validation failed: {}", e)))?;
let staged_credential = if touches_users || touches_user_enabled {
let secret = cfg
.access
.users
.get(user)
.ok_or_else(|| ApiFailure::internal("updated user secret is missing"))?;
Some(
credential_id_from_hex(secret)
.ok_or_else(|| ApiFailure::internal("validated user secret could not be decoded"))?,
)
} else {
None
};
let staged_credential =
if touches_users || touches_user_enabled {
let secret = cfg
.access
.users
.get(user)
.ok_or_else(|| ApiFailure::internal("updated user secret is missing"))?;
Some(credential_id_from_hex(secret).ok_or_else(|| {
ApiFailure::internal("validated user secret could not be decoded")
})?)
} else {
None
};
let mut touched_sections = Vec::new();
if touches_users {
@@ -206,9 +206,11 @@ async fn patch_user_to_completion(
.await?
};
if let Some(credential_id) = staged_credential {
shared
.proxy_shared
.stage_user_credential(user, credential_id, cfg.access.is_user_enabled(user));
shared.proxy_shared.stage_user_credential(
user,
credential_id,
cfg.access.is_user_enabled(user),
);
}
match max_unique_ips_change {
Some(Some(limit)) => shared.ip_tracker.set_user_limit(user, limit).await,