Autodetect IP in API User-links

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey 2026-03-04 11:04:54 +03:00
parent 2510ebaa79
commit 5df2fe9f97
No known key found for this signature in database
3 changed files with 90 additions and 11 deletions

View File

@ -1,5 +1,5 @@
use std::convert::Infallible; use std::convert::Infallible;
use std::net::SocketAddr; use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
@ -43,6 +43,8 @@ pub(super) struct ApiShared {
pub(super) ip_tracker: Arc<UserIpTracker>, pub(super) ip_tracker: Arc<UserIpTracker>,
pub(super) me_pool: Option<Arc<MePool>>, pub(super) me_pool: Option<Arc<MePool>>,
pub(super) config_path: PathBuf, pub(super) config_path: PathBuf,
pub(super) startup_detected_ip_v4: Option<IpAddr>,
pub(super) startup_detected_ip_v6: Option<IpAddr>,
pub(super) mutation_lock: Arc<Mutex<()>>, pub(super) mutation_lock: Arc<Mutex<()>>,
pub(super) minimal_cache: Arc<Mutex<Option<MinimalCacheEntry>>>, pub(super) minimal_cache: Arc<Mutex<Option<MinimalCacheEntry>>>,
pub(super) request_id: Arc<AtomicU64>, pub(super) request_id: Arc<AtomicU64>,
@ -61,6 +63,8 @@ pub async fn serve(
me_pool: Option<Arc<MePool>>, me_pool: Option<Arc<MePool>>,
config_rx: watch::Receiver<Arc<ProxyConfig>>, config_rx: watch::Receiver<Arc<ProxyConfig>>,
config_path: PathBuf, config_path: PathBuf,
startup_detected_ip_v4: Option<IpAddr>,
startup_detected_ip_v6: Option<IpAddr>,
) { ) {
let listener = match TcpListener::bind(listen).await { let listener = match TcpListener::bind(listen).await {
Ok(listener) => listener, Ok(listener) => listener,
@ -81,6 +85,8 @@ pub async fn serve(
ip_tracker, ip_tracker,
me_pool, me_pool,
config_path, config_path,
startup_detected_ip_v4,
startup_detected_ip_v6,
mutation_lock: Arc::new(Mutex::new(())), mutation_lock: Arc::new(Mutex::new(())),
minimal_cache: Arc::new(Mutex::new(None)), minimal_cache: Arc::new(Mutex::new(None)),
request_id: Arc::new(AtomicU64::new(1)), request_id: Arc::new(AtomicU64::new(1)),
@ -212,7 +218,14 @@ async fn handle(
} }
("GET", "/v1/stats/users") | ("GET", "/v1/users") => { ("GET", "/v1/stats/users") | ("GET", "/v1/users") => {
let revision = current_revision(&shared.config_path).await?; 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)) Ok(success_response(StatusCode::OK, users, revision))
} }
("POST", "/v1/users") => { ("POST", "/v1/users") => {
@ -238,7 +251,14 @@ async fn handle(
{ {
if method == Method::GET { if method == Method::GET {
let revision = current_revision(&shared.config_path).await?; 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) if let Some(user_info) = users.into_iter().find(|entry| entry.username == user)
{ {
return Ok(success_response(StatusCode::OK, user_info, revision)); return Ok(success_response(StatusCode::OK, user_info, revision));

View File

@ -92,7 +92,14 @@ pub(super) async fn create_user(
shared.ip_tracker.set_user_limit(&body.username, limit).await; 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 let user = users
.into_iter() .into_iter()
.find(|entry| entry.username == body.username) .find(|entry| entry.username == body.username)
@ -106,7 +113,12 @@ pub(super) async fn create_user(
current_connections: 0, current_connections: 0,
active_unique_ips: 0, active_unique_ips: 0,
total_octets: 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)) Ok((CreateUserResponse { user, secret }, revision))
@ -171,7 +183,14 @@ pub(super) async fn patch_user(
if let Some(limit) = updated_limit { if let Some(limit) = updated_limit {
shared.ip_tracker.set_user_limit(user, limit).await; 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 let user_info = users
.into_iter() .into_iter()
.find(|entry| entry.username == user) .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?; let revision = save_config_to_disk(&shared.config_path, &cfg).await?;
drop(_guard); 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 let user_info = users
.into_iter() .into_iter()
.find(|entry| entry.username == user) .find(|entry| entry.username == user)
@ -270,6 +296,8 @@ pub(super) async fn users_from_config(
cfg: &ProxyConfig, cfg: &ProxyConfig,
stats: &Stats, stats: &Stats,
ip_tracker: &UserIpTracker, ip_tracker: &UserIpTracker,
startup_detected_ip_v4: Option<IpAddr>,
startup_detected_ip_v6: Option<IpAddr>,
) -> Vec<UserInfo> { ) -> Vec<UserInfo> {
let ip_counts = ip_tracker let ip_counts = ip_tracker
.get_stats() .get_stats()
@ -287,7 +315,14 @@ pub(super) async fn users_from_config(
.access .access
.users .users
.get(&username) .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 { .unwrap_or(UserLinks {
classic: Vec::new(), classic: Vec::new(),
secure: Vec::new(), secure: Vec::new(),
@ -313,8 +348,13 @@ pub(super) async fn users_from_config(
users users
} }
fn build_user_links(cfg: &ProxyConfig, secret: &str) -> UserLinks { fn build_user_links(
let hosts = resolve_link_hosts(cfg); cfg: &ProxyConfig,
secret: &str,
startup_detected_ip_v4: Option<IpAddr>,
startup_detected_ip_v6: Option<IpAddr>,
) -> 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 port = cfg.general.links.public_port.unwrap_or(cfg.server.port);
let tls_domains = resolve_tls_domains(cfg); 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<String> { fn resolve_link_hosts(
cfg: &ProxyConfig,
startup_detected_ip_v4: Option<IpAddr>,
startup_detected_ip_v6: Option<IpAddr>,
) -> Vec<String> {
if let Some(host) = cfg if let Some(host) = cfg
.general .general
.links .links
@ -365,6 +409,17 @@ fn resolve_link_hosts(cfg: &ProxyConfig) -> Vec<String> {
return vec![host.to_string()]; 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(); let mut hosts = Vec::new();
for listener in &cfg.server.listeners { for listener in &cfg.server.listeners {
if let Some(host) = listener if let Some(host) = listener

View File

@ -1171,6 +1171,8 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let me_pool_api = me_pool.clone(); let me_pool_api = me_pool.clone();
let config_rx_api = config_rx.clone(); let config_rx_api = config_rx.clone();
let config_path_api = std::path::PathBuf::from(&config_path); 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 { tokio::spawn(async move {
api::serve( api::serve(
listen, listen,
@ -1179,6 +1181,8 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
me_pool_api, me_pool_api,
config_rx_api, config_rx_api,
config_path_api, config_path_api,
startup_detected_ip_v4,
startup_detected_ip_v6,
) )
.await; .await;
}); });