From 5df2fe9f97f5ccc3c48616973e5442f6c44e8c4b Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:04:54 +0300 Subject: [PATCH 1/4] Autodetect IP in API User-links Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/api/mod.rs | 26 ++++++++++++++++-- src/api/users.rs | 71 ++++++++++++++++++++++++++++++++++++++++++------ src/main.rs | 4 +++ 3 files changed, 90 insertions(+), 11 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 299d5a1..55d790f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ use std::convert::Infallible; -use std::net::SocketAddr; +use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; @@ -43,6 +43,8 @@ pub(super) struct ApiShared { pub(super) ip_tracker: Arc, pub(super) me_pool: Option>, pub(super) config_path: PathBuf, + pub(super) startup_detected_ip_v4: Option, + pub(super) startup_detected_ip_v6: Option, pub(super) mutation_lock: Arc>, pub(super) minimal_cache: Arc>>, pub(super) request_id: Arc, @@ -61,6 +63,8 @@ pub async fn serve( me_pool: Option>, config_rx: watch::Receiver>, config_path: PathBuf, + startup_detected_ip_v4: Option, + startup_detected_ip_v6: Option, ) { let listener = match TcpListener::bind(listen).await { Ok(listener) => listener, @@ -81,6 +85,8 @@ pub async fn serve( ip_tracker, me_pool, config_path, + startup_detected_ip_v4, + startup_detected_ip_v6, mutation_lock: Arc::new(Mutex::new(())), minimal_cache: Arc::new(Mutex::new(None)), request_id: Arc::new(AtomicU64::new(1)), @@ -212,7 +218,14 @@ async fn handle( } ("GET", "/v1/stats/users") | ("GET", "/v1/users") => { let revision = current_revision(&shared.config_path).await?; - let users = users_from_config(&cfg, &shared.stats, &shared.ip_tracker).await; + let users = users_from_config( + &cfg, + &shared.stats, + &shared.ip_tracker, + shared.startup_detected_ip_v4, + shared.startup_detected_ip_v6, + ) + .await; Ok(success_response(StatusCode::OK, users, revision)) } ("POST", "/v1/users") => { @@ -238,7 +251,14 @@ async fn handle( { if method == Method::GET { let revision = current_revision(&shared.config_path).await?; - let users = users_from_config(&cfg, &shared.stats, &shared.ip_tracker).await; + let users = users_from_config( + &cfg, + &shared.stats, + &shared.ip_tracker, + shared.startup_detected_ip_v4, + shared.startup_detected_ip_v6, + ) + .await; if let Some(user_info) = users.into_iter().find(|entry| entry.username == user) { return Ok(success_response(StatusCode::OK, user_info, revision)); diff --git a/src/api/users.rs b/src/api/users.rs index 9fc03e9..c907070 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -92,7 +92,14 @@ pub(super) async fn create_user( shared.ip_tracker.set_user_limit(&body.username, limit).await; } - let users = users_from_config(&cfg, &shared.stats, &shared.ip_tracker).await; + let users = users_from_config( + &cfg, + &shared.stats, + &shared.ip_tracker, + shared.startup_detected_ip_v4, + shared.startup_detected_ip_v6, + ) + .await; let user = users .into_iter() .find(|entry| entry.username == body.username) @@ -106,7 +113,12 @@ pub(super) async fn create_user( current_connections: 0, active_unique_ips: 0, total_octets: 0, - links: build_user_links(&cfg, &secret), + links: build_user_links( + &cfg, + &secret, + shared.startup_detected_ip_v4, + shared.startup_detected_ip_v6, + ), }); Ok((CreateUserResponse { user, secret }, revision)) @@ -171,7 +183,14 @@ pub(super) async fn patch_user( if let Some(limit) = updated_limit { shared.ip_tracker.set_user_limit(user, limit).await; } - let users = users_from_config(&cfg, &shared.stats, &shared.ip_tracker).await; + let users = users_from_config( + &cfg, + &shared.stats, + &shared.ip_tracker, + shared.startup_detected_ip_v4, + shared.startup_detected_ip_v6, + ) + .await; let user_info = users .into_iter() .find(|entry| entry.username == user) @@ -211,7 +230,14 @@ pub(super) async fn rotate_secret( let revision = save_config_to_disk(&shared.config_path, &cfg).await?; drop(_guard); - let users = users_from_config(&cfg, &shared.stats, &shared.ip_tracker).await; + let users = users_from_config( + &cfg, + &shared.stats, + &shared.ip_tracker, + shared.startup_detected_ip_v4, + shared.startup_detected_ip_v6, + ) + .await; let user_info = users .into_iter() .find(|entry| entry.username == user) @@ -270,6 +296,8 @@ pub(super) async fn users_from_config( cfg: &ProxyConfig, stats: &Stats, ip_tracker: &UserIpTracker, + startup_detected_ip_v4: Option, + startup_detected_ip_v6: Option, ) -> Vec { let ip_counts = ip_tracker .get_stats() @@ -287,7 +315,14 @@ pub(super) async fn users_from_config( .access .users .get(&username) - .map(|secret| build_user_links(cfg, secret)) + .map(|secret| { + build_user_links( + cfg, + secret, + startup_detected_ip_v4, + startup_detected_ip_v6, + ) + }) .unwrap_or(UserLinks { classic: Vec::new(), secure: Vec::new(), @@ -313,8 +348,13 @@ pub(super) async fn users_from_config( users } -fn build_user_links(cfg: &ProxyConfig, secret: &str) -> UserLinks { - let hosts = resolve_link_hosts(cfg); +fn build_user_links( + cfg: &ProxyConfig, + secret: &str, + startup_detected_ip_v4: Option, + startup_detected_ip_v6: Option, +) -> UserLinks { + let hosts = resolve_link_hosts(cfg, startup_detected_ip_v4, startup_detected_ip_v6); let port = cfg.general.links.public_port.unwrap_or(cfg.server.port); let tls_domains = resolve_tls_domains(cfg); @@ -353,7 +393,11 @@ fn build_user_links(cfg: &ProxyConfig, secret: &str) -> UserLinks { } } -fn resolve_link_hosts(cfg: &ProxyConfig) -> Vec { +fn resolve_link_hosts( + cfg: &ProxyConfig, + startup_detected_ip_v4: Option, + startup_detected_ip_v6: Option, +) -> Vec { if let Some(host) = cfg .general .links @@ -365,6 +409,17 @@ fn resolve_link_hosts(cfg: &ProxyConfig) -> Vec { return vec![host.to_string()]; } + let mut startup_hosts = Vec::new(); + if let Some(ip) = startup_detected_ip_v4 { + push_unique_host(&mut startup_hosts, &ip.to_string()); + } + if let Some(ip) = startup_detected_ip_v6 { + push_unique_host(&mut startup_hosts, &ip.to_string()); + } + if !startup_hosts.is_empty() { + return startup_hosts; + } + let mut hosts = Vec::new(); for listener in &cfg.server.listeners { if let Some(host) = listener diff --git a/src/main.rs b/src/main.rs index c4f0e68..1845fdb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1171,6 +1171,8 @@ async fn main() -> std::result::Result<(), Box> { let me_pool_api = me_pool.clone(); let config_rx_api = config_rx.clone(); let config_path_api = std::path::PathBuf::from(&config_path); + let startup_detected_ip_v4 = detected_ip_v4; + let startup_detected_ip_v6 = detected_ip_v6; tokio::spawn(async move { api::serve( listen, @@ -1179,6 +1181,8 @@ async fn main() -> std::result::Result<(), Box> { me_pool_api, config_rx_api, config_path_api, + startup_detected_ip_v4, + startup_detected_ip_v6, ) .await; }); From de2047adf269e52b82c3519d64319cf0af88a1b0 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:41:41 +0300 Subject: [PATCH 2/4] API UpstreamManager Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/api/mod.rs | 11 ++- src/api/model.rs | 44 ++++++++++++ src/api/runtime_stats.rs | 146 +++++++++++++++++++++++++++++++------- src/main.rs | 2 + src/transport/upstream.rs | 95 +++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 28 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 55d790f..c01566a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -20,6 +20,7 @@ use crate::config::ProxyConfig; use crate::ip_tracker::UserIpTracker; use crate::stats::Stats; use crate::transport::middle_proxy::MePool; +use crate::transport::UpstreamManager; mod config_store; mod model; @@ -33,7 +34,7 @@ use model::{ }; use runtime_stats::{ MinimalCacheEntry, build_dcs_data, build_me_writers_data, build_minimal_all_data, - build_zero_all_data, + build_upstreams_data, build_zero_all_data, }; use users::{create_user, delete_user, patch_user, rotate_secret, users_from_config}; @@ -42,6 +43,7 @@ pub(super) struct ApiShared { pub(super) stats: Arc, pub(super) ip_tracker: Arc, pub(super) me_pool: Option>, + pub(super) upstream_manager: Arc, pub(super) config_path: PathBuf, pub(super) startup_detected_ip_v4: Option, pub(super) startup_detected_ip_v6: Option, @@ -61,6 +63,7 @@ pub async fn serve( stats: Arc, ip_tracker: Arc, me_pool: Option>, + upstream_manager: Arc, config_rx: watch::Receiver>, config_path: PathBuf, startup_detected_ip_v4: Option, @@ -84,6 +87,7 @@ pub async fn serve( stats, ip_tracker, me_pool, + upstream_manager, config_path, startup_detected_ip_v4, startup_detected_ip_v6, @@ -201,6 +205,11 @@ async fn handle( let data = build_zero_all_data(&shared.stats, cfg.access.users.len()); Ok(success_response(StatusCode::OK, data, revision)) } + ("GET", "/v1/stats/upstreams") => { + let revision = current_revision(&shared.config_path).await?; + let data = build_upstreams_data(shared.as_ref(), api_cfg); + Ok(success_response(StatusCode::OK, data, revision)) + } ("GET", "/v1/stats/minimal/all") => { let revision = current_revision(&shared.config_path).await?; let data = build_minimal_all_data(shared.as_ref(), api_cfg).await; diff --git a/src/api/model.rs b/src/api/model.rs index be76c4e..efe8ebb 100644 --- a/src/api/model.rs +++ b/src/api/model.rs @@ -103,6 +103,50 @@ pub(super) struct ZeroUpstreamData { pub(super) connect_duration_fail_bucket_gt_1000ms: u64, } +#[derive(Serialize, Clone)] +pub(super) struct UpstreamDcStatus { + pub(super) dc: i16, + pub(super) latency_ema_ms: Option, + pub(super) ip_preference: &'static str, +} + +#[derive(Serialize, Clone)] +pub(super) struct UpstreamStatus { + pub(super) upstream_id: usize, + pub(super) route_kind: &'static str, + pub(super) address: String, + pub(super) weight: u16, + pub(super) scopes: String, + pub(super) healthy: bool, + pub(super) fails: u32, + pub(super) last_check_age_secs: u64, + pub(super) effective_latency_ms: Option, + pub(super) dc: Vec, +} + +#[derive(Serialize, Clone)] +pub(super) struct UpstreamSummaryData { + pub(super) configured_total: usize, + pub(super) healthy_total: usize, + pub(super) unhealthy_total: usize, + pub(super) direct_total: usize, + pub(super) socks4_total: usize, + pub(super) socks5_total: usize, +} + +#[derive(Serialize, Clone)] +pub(super) struct UpstreamsData { + pub(super) enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub(super) reason: Option<&'static str>, + pub(super) generated_at_epoch_secs: u64, + pub(super) zero: ZeroUpstreamData, + #[serde(skip_serializing_if = "Option::is_none")] + pub(super) summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(super) upstreams: Option>, +} + #[derive(Serialize, Clone)] pub(super) struct ZeroMiddleProxyData { pub(super) keepalive_sent_total: u64, diff --git a/src/api/runtime_stats.rs b/src/api/runtime_stats.rs index 53fdeff..3019636 100644 --- a/src/api/runtime_stats.rs +++ b/src/api/runtime_stats.rs @@ -2,12 +2,15 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use crate::config::ApiConfig; use crate::stats::Stats; +use crate::transport::upstream::IpPreference; +use crate::transport::UpstreamRouteKind; use super::ApiShared; use super::model::{ DcStatus, DcStatusData, MeWriterStatus, MeWritersData, MeWritersSummary, MinimalAllData, MinimalAllPayload, MinimalDcPathData, MinimalMeRuntimeData, MinimalQuarantineData, - ZeroAllData, ZeroCodeCount, ZeroCoreData, ZeroDesyncData, ZeroMiddleProxyData, ZeroPoolData, + UpstreamDcStatus, UpstreamStatus, UpstreamSummaryData, UpstreamsData, ZeroAllData, + ZeroCodeCount, ZeroCoreData, ZeroDesyncData, ZeroMiddleProxyData, ZeroPoolData, ZeroUpstreamData, }; @@ -41,32 +44,7 @@ pub(super) fn build_zero_all_data(stats: &Stats, configured_users: usize) -> Zer telemetry_user_enabled: telemetry.user_enabled, telemetry_me_level: telemetry.me_level.to_string(), }, - upstream: ZeroUpstreamData { - connect_attempt_total: stats.get_upstream_connect_attempt_total(), - connect_success_total: stats.get_upstream_connect_success_total(), - connect_fail_total: stats.get_upstream_connect_fail_total(), - connect_failfast_hard_error_total: stats.get_upstream_connect_failfast_hard_error_total(), - connect_attempts_bucket_1: stats.get_upstream_connect_attempts_bucket_1(), - connect_attempts_bucket_2: stats.get_upstream_connect_attempts_bucket_2(), - connect_attempts_bucket_3_4: stats.get_upstream_connect_attempts_bucket_3_4(), - connect_attempts_bucket_gt_4: stats.get_upstream_connect_attempts_bucket_gt_4(), - connect_duration_success_bucket_le_100ms: stats - .get_upstream_connect_duration_success_bucket_le_100ms(), - connect_duration_success_bucket_101_500ms: stats - .get_upstream_connect_duration_success_bucket_101_500ms(), - connect_duration_success_bucket_501_1000ms: stats - .get_upstream_connect_duration_success_bucket_501_1000ms(), - connect_duration_success_bucket_gt_1000ms: stats - .get_upstream_connect_duration_success_bucket_gt_1000ms(), - connect_duration_fail_bucket_le_100ms: stats - .get_upstream_connect_duration_fail_bucket_le_100ms(), - connect_duration_fail_bucket_101_500ms: stats - .get_upstream_connect_duration_fail_bucket_101_500ms(), - connect_duration_fail_bucket_501_1000ms: stats - .get_upstream_connect_duration_fail_bucket_501_1000ms(), - connect_duration_fail_bucket_gt_1000ms: stats - .get_upstream_connect_duration_fail_bucket_gt_1000ms(), - }, + upstream: build_zero_upstream_data(stats), middle_proxy: ZeroMiddleProxyData { keepalive_sent_total: stats.get_me_keepalive_sent(), keepalive_failed_total: stats.get_me_keepalive_failed(), @@ -140,6 +118,102 @@ pub(super) fn build_zero_all_data(stats: &Stats, configured_users: usize) -> Zer } } +fn build_zero_upstream_data(stats: &Stats) -> ZeroUpstreamData { + ZeroUpstreamData { + connect_attempt_total: stats.get_upstream_connect_attempt_total(), + connect_success_total: stats.get_upstream_connect_success_total(), + connect_fail_total: stats.get_upstream_connect_fail_total(), + connect_failfast_hard_error_total: stats.get_upstream_connect_failfast_hard_error_total(), + connect_attempts_bucket_1: stats.get_upstream_connect_attempts_bucket_1(), + connect_attempts_bucket_2: stats.get_upstream_connect_attempts_bucket_2(), + connect_attempts_bucket_3_4: stats.get_upstream_connect_attempts_bucket_3_4(), + connect_attempts_bucket_gt_4: stats.get_upstream_connect_attempts_bucket_gt_4(), + connect_duration_success_bucket_le_100ms: stats + .get_upstream_connect_duration_success_bucket_le_100ms(), + connect_duration_success_bucket_101_500ms: stats + .get_upstream_connect_duration_success_bucket_101_500ms(), + connect_duration_success_bucket_501_1000ms: stats + .get_upstream_connect_duration_success_bucket_501_1000ms(), + connect_duration_success_bucket_gt_1000ms: stats + .get_upstream_connect_duration_success_bucket_gt_1000ms(), + connect_duration_fail_bucket_le_100ms: stats.get_upstream_connect_duration_fail_bucket_le_100ms(), + connect_duration_fail_bucket_101_500ms: stats + .get_upstream_connect_duration_fail_bucket_101_500ms(), + connect_duration_fail_bucket_501_1000ms: stats + .get_upstream_connect_duration_fail_bucket_501_1000ms(), + connect_duration_fail_bucket_gt_1000ms: stats + .get_upstream_connect_duration_fail_bucket_gt_1000ms(), + } +} + +pub(super) fn build_upstreams_data(shared: &ApiShared, api_cfg: &ApiConfig) -> UpstreamsData { + let generated_at_epoch_secs = now_epoch_secs(); + let zero = build_zero_upstream_data(&shared.stats); + if !api_cfg.minimal_runtime_enabled { + return UpstreamsData { + enabled: false, + reason: Some(FEATURE_DISABLED_REASON), + generated_at_epoch_secs, + zero, + summary: None, + upstreams: None, + }; + } + + let Some(snapshot) = shared.upstream_manager.try_api_snapshot() else { + return UpstreamsData { + enabled: true, + reason: Some(SOURCE_UNAVAILABLE_REASON), + generated_at_epoch_secs, + zero, + summary: None, + upstreams: None, + }; + }; + + let summary = UpstreamSummaryData { + configured_total: snapshot.summary.configured_total, + healthy_total: snapshot.summary.healthy_total, + unhealthy_total: snapshot.summary.unhealthy_total, + direct_total: snapshot.summary.direct_total, + socks4_total: snapshot.summary.socks4_total, + socks5_total: snapshot.summary.socks5_total, + }; + let upstreams = snapshot + .upstreams + .into_iter() + .map(|upstream| UpstreamStatus { + upstream_id: upstream.upstream_id, + route_kind: map_route_kind(upstream.route_kind), + address: upstream.address, + weight: upstream.weight, + scopes: upstream.scopes, + healthy: upstream.healthy, + fails: upstream.fails, + last_check_age_secs: upstream.last_check_age_secs, + effective_latency_ms: upstream.effective_latency_ms, + dc: upstream + .dc + .into_iter() + .map(|dc| UpstreamDcStatus { + dc: dc.dc, + latency_ema_ms: dc.latency_ema_ms, + ip_preference: map_ip_preference(dc.ip_preference), + }) + .collect(), + }) + .collect(); + + UpstreamsData { + enabled: true, + reason: None, + generated_at_epoch_secs, + zero, + summary: Some(summary), + upstreams: Some(upstreams), + } +} + pub(super) async fn build_minimal_all_data( shared: &ApiShared, api_cfg: &ApiConfig, @@ -384,6 +458,24 @@ fn disabled_dcs(now_epoch_secs: u64, reason: &'static str) -> DcStatusData { } } +fn map_route_kind(value: UpstreamRouteKind) -> &'static str { + match value { + UpstreamRouteKind::Direct => "direct", + UpstreamRouteKind::Socks4 => "socks4", + UpstreamRouteKind::Socks5 => "socks5", + } +} + +fn map_ip_preference(value: IpPreference) -> &'static str { + match value { + IpPreference::Unknown => "unknown", + IpPreference::PreferV6 => "prefer_v6", + IpPreference::PreferV4 => "prefer_v4", + IpPreference::BothWork => "both_work", + IpPreference::Unavailable => "unavailable", + } +} + fn now_epoch_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/src/main.rs b/src/main.rs index 1845fdb..0aec195 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1169,6 +1169,7 @@ async fn main() -> std::result::Result<(), Box> { let stats = stats.clone(); let ip_tracker_api = ip_tracker.clone(); let me_pool_api = me_pool.clone(); + let upstream_manager_api = upstream_manager.clone(); let config_rx_api = config_rx.clone(); let config_path_api = std::path::PathBuf::from(&config_path); let startup_detected_ip_v4 = detected_ip_v4; @@ -1179,6 +1180,7 @@ async fn main() -> std::result::Result<(), Box> { stats, ip_tracker_api, me_pool_api, + upstream_manager_api, config_rx_api, config_path_api, startup_detected_ip_v4, diff --git a/src/transport/upstream.rs b/src/transport/upstream.rs index 1e2dd1e..d9f0ede 100644 --- a/src/transport/upstream.rs +++ b/src/transport/upstream.rs @@ -165,6 +165,43 @@ pub enum UpstreamRouteKind { Socks5, } +#[derive(Debug, Clone)] +pub struct UpstreamApiDcSnapshot { + pub dc: i16, + pub latency_ema_ms: Option, + pub ip_preference: IpPreference, +} + +#[derive(Debug, Clone)] +pub struct UpstreamApiItemSnapshot { + pub upstream_id: usize, + pub route_kind: UpstreamRouteKind, + pub address: String, + pub weight: u16, + pub scopes: String, + pub healthy: bool, + pub fails: u32, + pub last_check_age_secs: u64, + pub effective_latency_ms: Option, + pub dc: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct UpstreamApiSummarySnapshot { + pub configured_total: usize, + pub healthy_total: usize, + pub unhealthy_total: usize, + pub direct_total: usize, + pub socks4_total: usize, + pub socks5_total: usize, +} + +#[derive(Debug, Clone)] +pub struct UpstreamApiSnapshot { + pub summary: UpstreamApiSummarySnapshot, + pub upstreams: Vec, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct UpstreamEgressInfo { pub route_kind: UpstreamRouteKind, @@ -217,6 +254,64 @@ impl UpstreamManager { } } + pub fn try_api_snapshot(&self) -> Option { + let guard = self.upstreams.try_read().ok()?; + let now = std::time::Instant::now(); + + let mut summary = UpstreamApiSummarySnapshot { + configured_total: guard.len(), + ..UpstreamApiSummarySnapshot::default() + }; + let mut upstreams = Vec::with_capacity(guard.len()); + + for (idx, upstream) in guard.iter().enumerate() { + if upstream.healthy { + summary.healthy_total += 1; + } else { + summary.unhealthy_total += 1; + } + + let (route_kind, address) = match &upstream.config.upstream_type { + UpstreamType::Direct { .. } => { + summary.direct_total += 1; + (UpstreamRouteKind::Direct, "direct".to_string()) + } + UpstreamType::Socks4 { address, .. } => { + summary.socks4_total += 1; + (UpstreamRouteKind::Socks4, address.clone()) + } + UpstreamType::Socks5 { address, .. } => { + summary.socks5_total += 1; + (UpstreamRouteKind::Socks5, address.clone()) + } + }; + + let mut dc = Vec::with_capacity(NUM_DCS); + for dc_idx in 0..NUM_DCS { + dc.push(UpstreamApiDcSnapshot { + dc: (dc_idx + 1) as i16, + latency_ema_ms: upstream.dc_latency[dc_idx].get(), + ip_preference: upstream.dc_ip_pref[dc_idx], + }); + } + + upstreams.push(UpstreamApiItemSnapshot { + upstream_id: idx, + route_kind, + address, + weight: upstream.config.weight, + scopes: upstream.config.scopes.clone(), + healthy: upstream.healthy, + fails: upstream.fails, + last_check_age_secs: now.saturating_duration_since(upstream.last_check).as_secs(), + effective_latency_ms: upstream.effective_latency(None), + dc, + }); + } + + Some(UpstreamApiSnapshot { summary, upstreams }) + } + #[cfg(unix)] fn resolve_interface_addrs(name: &str, want_ipv6: bool) -> Vec { use nix::ifaddrs::getifaddrs; From 173624c838863030eb0e61446bd9d5b867fc6dd3 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:44:50 +0300 Subject: [PATCH 3/4] Update Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 324af49..d38431d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "telemt" -version = "3.2.0" +version = "3.2.1" edition = "2024" [dependencies] From dbadbf0221ebed1bea3aa851bb528cb2a69147f5 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:45:32 +0300 Subject: [PATCH 4/4] Update config.toml --- config.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config.toml b/config.toml index cb33e3d..ab35789 100644 --- a/config.toml +++ b/config.toml @@ -34,6 +34,13 @@ port = 443 # metrics_port = 9090 # metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"] +[server.api] +enabled = true +listen = "0.0.0.0:9091" +whitelist = ["127.0.0.0/8"] +minimal_runtime_enabled = false +minimal_runtime_cache_ttl_ms = 1000 + # Listen on multiple interfaces/IPs - IPv4 [[server.listeners]] ip = "0.0.0.0"