From f0e1a6cf1c491b55b53061e89027d1f29385e216 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:34:47 +0300 Subject: [PATCH] Expose tls_domains links as domain-link pairs Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> Signed-off-by: Alexey <247128645+axkurcom@users.noreply.github.com> --- src/api/model.rs | 7 ++++ src/api/users.rs | 106 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/api/model.rs b/src/api/model.rs index 1ca9f33..1604491 100644 --- a/src/api/model.rs +++ b/src/api/model.rs @@ -456,6 +456,13 @@ pub(super) struct UserLinks { pub(super) classic: Vec, pub(super) secure: Vec, pub(super) tls: Vec, + pub(super) tls_domains: Vec, +} + +#[derive(Serialize)] +pub(super) struct TlsDomainLink { + pub(super) domain: String, + pub(super) link: String, } #[derive(Serialize)] diff --git a/src/api/users.rs b/src/api/users.rs index df89273..cb0ffb0 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -13,7 +13,7 @@ use super::config_store::{ }; use super::model::{ ApiFailure, CreateUserRequest, CreateUserResponse, PatchUserRequest, RotateSecretRequest, - UserInfo, UserLinks, is_valid_ad_tag, is_valid_user_secret, is_valid_username, + TlsDomainLink, UserInfo, UserLinks, is_valid_ad_tag, is_valid_user_secret, is_valid_username, parse_optional_expiration, parse_patch_expiration, random_user_secret, }; use super::patch::Patch; @@ -469,6 +469,7 @@ pub(super) async fn users_from_config( classic: Vec::new(), secure: Vec::new(), tls: Vec::new(), + tls_domains: Vec::new(), }); users.push(UserInfo { in_runtime: runtime_cfg @@ -523,10 +524,12 @@ fn build_user_links( .public_port .unwrap_or(resolve_default_link_port(cfg)); let tls_domains = resolve_tls_domains(cfg); + let extra_tls_domains = resolve_extra_tls_domains(cfg); let mut classic = Vec::new(); let mut secure = Vec::new(); let mut tls = Vec::new(); + let mut tls_domain_links = Vec::new(); for host in &hosts { if cfg.general.modes.classic { @@ -549,6 +552,17 @@ fn build_user_links( host, port, secret, domain_hex )); } + for domain in &extra_tls_domains { + let domain_hex = hex::encode(domain); + let link = format!( + "tg://proxy?server={}&port={}&secret=ee{}{}", + host, port, secret, domain_hex + ); + tls_domain_links.push(TlsDomainLink { + domain: (*domain).to_string(), + link, + }); + } } } @@ -556,6 +570,7 @@ fn build_user_links( classic, secure, tls, + tls_domains: tls_domain_links, } } @@ -672,6 +687,19 @@ fn resolve_tls_domains(cfg: &ProxyConfig) -> Vec<&str> { domains } +fn resolve_extra_tls_domains(cfg: &ProxyConfig) -> Vec<&str> { + let mut domains = Vec::with_capacity(cfg.censorship.tls_domains.len()); + let primary = cfg.censorship.tls_domain.as_str(); + for domain in &cfg.censorship.tls_domains { + let value = domain.as_str(); + if value.is_empty() || value == primary || domains.contains(&value) { + continue; + } + domains.push(value); + } + domains +} + #[cfg(test)] mod tests { use super::*; @@ -761,4 +789,80 @@ mod tests { assert!(alice.in_runtime); assert!(!bob.in_runtime); } + + #[tokio::test] + async fn users_from_config_returns_tls_link_for_each_tls_domain() { + let mut cfg = ProxyConfig::default(); + cfg.access.users.insert( + "alice".to_string(), + "0123456789abcdef0123456789abcdef".to_string(), + ); + cfg.general.modes.classic = false; + cfg.general.modes.secure = false; + cfg.general.modes.tls = true; + cfg.general.links.public_host = Some("proxy.example.net".to_string()); + cfg.general.links.public_port = Some(443); + cfg.censorship.tls_domain = "front-a.example.com".to_string(); + cfg.censorship.tls_domains = vec![ + "front-b.example.com".to_string(), + "front-c.example.com".to_string(), + "front-b.example.com".to_string(), + "front-a.example.com".to_string(), + ]; + + let stats = Stats::new(); + let tracker = UserIpTracker::new(); + let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await; + let alice = users + .iter() + .find(|entry| entry.username == "alice") + .expect("alice must be present"); + + assert_eq!(alice.links.tls.len(), 3); + assert!( + alice + .links + .tls + .iter() + .any(|link| link.ends_with(&hex::encode("front-a.example.com"))) + ); + assert!( + alice + .links + .tls + .iter() + .any(|link| link.ends_with(&hex::encode("front-b.example.com"))) + ); + assert!( + alice + .links + .tls + .iter() + .any(|link| link.ends_with(&hex::encode("front-c.example.com"))) + ); + assert_eq!(alice.links.tls_domains.len(), 2); + assert!( + alice + .links + .tls_domains + .iter() + .any(|entry| entry.domain == "front-b.example.com" + && entry.link.ends_with(&hex::encode("front-b.example.com"))) + ); + assert!( + alice + .links + .tls_domains + .iter() + .any(|entry| entry.domain == "front-c.example.com" + && entry.link.ends_with(&hex::encode("front-c.example.com"))) + ); + assert!( + !alice + .links + .tls_domains + .iter() + .any(|entry| entry.domain == "front-a.example.com") + ); + } }