mirror of
https://github.com/telemt/telemt.git
synced 2026-06-19 01:11:09 +03:00
Compare commits
6 Commits
236bbb4970
...
3.4.9
| Author | SHA1 | Date | |
|---|---|---|---|
| b1c947e8e3 | |||
| cfe01dced2 | |||
| 8520955a5f | |||
| 065786b839 | |||
| f0e1a6cf1c | |||
| 893cef22e3 |
Generated
+1
-1
@@ -2791,7 +2791,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.4.8"
|
version = "3.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.4.8"
|
version = "3.4.9"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ Monero (XMR) directly:
|
|||||||
8Bk4tZEYPQWSypeD2hrUXG2rKbAKF16GqEN942ZdAP5cFdSqW6h4DwkP5cJMAdszzuPeHeHZPTyjWWFwzeFdjuci3ktfMoB
|
8Bk4tZEYPQWSypeD2hrUXG2rKbAKF16GqEN942ZdAP5cFdSqW6h4DwkP5cJMAdszzuPeHeHZPTyjWWFwzeFdjuci3ktfMoB
|
||||||
```
|
```
|
||||||
|
|
||||||
All donations go toward infrastructure, development, and research.
|
All donations go toward infrastructure, development and research
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -456,6 +456,13 @@ pub(super) struct UserLinks {
|
|||||||
pub(super) classic: Vec<String>,
|
pub(super) classic: Vec<String>,
|
||||||
pub(super) secure: Vec<String>,
|
pub(super) secure: Vec<String>,
|
||||||
pub(super) tls: Vec<String>,
|
pub(super) tls: Vec<String>,
|
||||||
|
pub(super) tls_domains: Vec<TlsDomainLink>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(super) struct TlsDomainLink {
|
||||||
|
pub(super) domain: String,
|
||||||
|
pub(super) link: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|||||||
+105
-1
@@ -13,7 +13,7 @@ use super::config_store::{
|
|||||||
};
|
};
|
||||||
use super::model::{
|
use super::model::{
|
||||||
ApiFailure, CreateUserRequest, CreateUserResponse, PatchUserRequest, RotateSecretRequest,
|
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,
|
parse_optional_expiration, parse_patch_expiration, random_user_secret,
|
||||||
};
|
};
|
||||||
use super::patch::Patch;
|
use super::patch::Patch;
|
||||||
@@ -469,6 +469,7 @@ pub(super) async fn users_from_config(
|
|||||||
classic: Vec::new(),
|
classic: Vec::new(),
|
||||||
secure: Vec::new(),
|
secure: Vec::new(),
|
||||||
tls: Vec::new(),
|
tls: Vec::new(),
|
||||||
|
tls_domains: Vec::new(),
|
||||||
});
|
});
|
||||||
users.push(UserInfo {
|
users.push(UserInfo {
|
||||||
in_runtime: runtime_cfg
|
in_runtime: runtime_cfg
|
||||||
@@ -523,10 +524,12 @@ fn build_user_links(
|
|||||||
.public_port
|
.public_port
|
||||||
.unwrap_or(resolve_default_link_port(cfg));
|
.unwrap_or(resolve_default_link_port(cfg));
|
||||||
let tls_domains = resolve_tls_domains(cfg);
|
let tls_domains = resolve_tls_domains(cfg);
|
||||||
|
let extra_tls_domains = resolve_extra_tls_domains(cfg);
|
||||||
|
|
||||||
let mut classic = Vec::new();
|
let mut classic = Vec::new();
|
||||||
let mut secure = Vec::new();
|
let mut secure = Vec::new();
|
||||||
let mut tls = Vec::new();
|
let mut tls = Vec::new();
|
||||||
|
let mut tls_domain_links = Vec::new();
|
||||||
|
|
||||||
for host in &hosts {
|
for host in &hosts {
|
||||||
if cfg.general.modes.classic {
|
if cfg.general.modes.classic {
|
||||||
@@ -549,6 +552,17 @@ fn build_user_links(
|
|||||||
host, port, secret, domain_hex
|
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,
|
classic,
|
||||||
secure,
|
secure,
|
||||||
tls,
|
tls,
|
||||||
|
tls_domains: tls_domain_links,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,6 +687,19 @@ fn resolve_tls_domains(cfg: &ProxyConfig) -> Vec<&str> {
|
|||||||
domains
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -761,4 +789,80 @@ mod tests {
|
|||||||
assert!(alice.in_runtime);
|
assert!(alice.in_runtime);
|
||||||
assert!(!bob.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")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,8 +403,7 @@ mod tests {
|
|||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let startup_cwd =
|
let startup_cwd = std::env::temp_dir().join(format!("telemt_runtime_base_systemd_{nonce}"));
|
||||||
std::env::temp_dir().join(format!("telemt_runtime_base_systemd_{nonce}"));
|
|
||||||
std::fs::create_dir_all(&startup_cwd).unwrap();
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
|
||||||
let resolved =
|
let resolved =
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ use crate::tls_front::TlsFrontCache;
|
|||||||
use crate::tls_front::fetcher::TlsFetchStrategy;
|
use crate::tls_front::fetcher::TlsFetchStrategy;
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
|
fn tls_fetch_host_for_domain(mask_host: &str, primary_tls_domain: &str, domain: &str) -> String {
|
||||||
|
if mask_host.eq_ignore_ascii_case(primary_tls_domain) {
|
||||||
|
domain.to_string()
|
||||||
|
} else {
|
||||||
|
mask_host.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn bootstrap_tls_front(
|
pub(crate) async fn bootstrap_tls_front(
|
||||||
config: &ProxyConfig,
|
config: &ProxyConfig,
|
||||||
tls_domains: &[String],
|
tls_domains: &[String],
|
||||||
@@ -56,6 +64,7 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let cache_initial = cache.clone();
|
let cache_initial = cache.clone();
|
||||||
let domains_initial = tls_domains.to_vec();
|
let domains_initial = tls_domains.to_vec();
|
||||||
let host_initial = mask_host.clone();
|
let host_initial = mask_host.clone();
|
||||||
|
let primary_initial = config.censorship.tls_domain.clone();
|
||||||
let unix_sock_initial = mask_unix_sock.clone();
|
let unix_sock_initial = mask_unix_sock.clone();
|
||||||
let scope_initial = tls_fetch_scope.clone();
|
let scope_initial = tls_fetch_scope.clone();
|
||||||
let upstream_initial = upstream_manager.clone();
|
let upstream_initial = upstream_manager.clone();
|
||||||
@@ -64,7 +73,8 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let mut join = tokio::task::JoinSet::new();
|
let mut join = tokio::task::JoinSet::new();
|
||||||
for domain in domains_initial {
|
for domain in domains_initial {
|
||||||
let cache_domain = cache_initial.clone();
|
let cache_domain = cache_initial.clone();
|
||||||
let host_domain = host_initial.clone();
|
let host_domain =
|
||||||
|
tls_fetch_host_for_domain(&host_initial, &primary_initial, &domain);
|
||||||
let unix_sock_domain = unix_sock_initial.clone();
|
let unix_sock_domain = unix_sock_initial.clone();
|
||||||
let scope_domain = scope_initial.clone();
|
let scope_domain = scope_initial.clone();
|
||||||
let upstream_domain = upstream_initial.clone();
|
let upstream_domain = upstream_initial.clone();
|
||||||
@@ -117,6 +127,7 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let cache_refresh = cache.clone();
|
let cache_refresh = cache.clone();
|
||||||
let domains_refresh = tls_domains.to_vec();
|
let domains_refresh = tls_domains.to_vec();
|
||||||
let host_refresh = mask_host.clone();
|
let host_refresh = mask_host.clone();
|
||||||
|
let primary_refresh = config.censorship.tls_domain.clone();
|
||||||
let unix_sock_refresh = mask_unix_sock.clone();
|
let unix_sock_refresh = mask_unix_sock.clone();
|
||||||
let scope_refresh = tls_fetch_scope.clone();
|
let scope_refresh = tls_fetch_scope.clone();
|
||||||
let upstream_refresh = upstream_manager.clone();
|
let upstream_refresh = upstream_manager.clone();
|
||||||
@@ -130,7 +141,8 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let mut join = tokio::task::JoinSet::new();
|
let mut join = tokio::task::JoinSet::new();
|
||||||
for domain in domains_refresh.clone() {
|
for domain in domains_refresh.clone() {
|
||||||
let cache_domain = cache_refresh.clone();
|
let cache_domain = cache_refresh.clone();
|
||||||
let host_domain = host_refresh.clone();
|
let host_domain =
|
||||||
|
tls_fetch_host_for_domain(&host_refresh, &primary_refresh, &domain);
|
||||||
let unix_sock_domain = unix_sock_refresh.clone();
|
let unix_sock_domain = unix_sock_refresh.clone();
|
||||||
let scope_domain = scope_refresh.clone();
|
let scope_domain = scope_refresh.clone();
|
||||||
let upstream_domain = upstream_refresh.clone();
|
let upstream_domain = upstream_refresh.clone();
|
||||||
@@ -186,3 +198,24 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
|
|
||||||
tls_cache
|
tls_cache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::tls_fetch_host_for_domain;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_fetch_host_uses_each_domain_when_mask_host_is_primary_default() {
|
||||||
|
assert_eq!(
|
||||||
|
tls_fetch_host_for_domain("a.com", "a.com", "b.com"),
|
||||||
|
"b.com"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_fetch_host_preserves_explicit_non_primary_mask_host() {
|
||||||
|
assert_eq!(
|
||||||
|
tls_fetch_host_for_domain("origin.example", "a.com", "b.com"),
|
||||||
|
"origin.example"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,6 +130,14 @@ impl TlsFrontCache {
|
|||||||
warn!(file = %name, "Skipping TLS cache entry with invalid domain");
|
warn!(file = %name, "Skipping TLS cache entry with invalid domain");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !cert_info_matches_domain(&cached) {
|
||||||
|
warn!(
|
||||||
|
file = %name,
|
||||||
|
domain = %cached.domain,
|
||||||
|
"Skipping TLS cache entry with mismatched certificate metadata"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// fetched_at is skipped during deserialization; approximate with file mtime if available.
|
// fetched_at is skipped during deserialization; approximate with file mtime if available.
|
||||||
if let Ok(meta) = entry.metadata().await
|
if let Ok(meta) = entry.metadata().await
|
||||||
&& let Ok(modified) = meta.modified()
|
&& let Ok(modified) = meta.modified()
|
||||||
@@ -209,10 +217,100 @@ impl TlsFrontCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cert_info_matches_domain(cached: &CachedTlsData) -> bool {
|
||||||
|
let Some(cert_info) = cached.cert_info.as_ref() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if !cert_info.san_names.is_empty() {
|
||||||
|
return cert_info
|
||||||
|
.san_names
|
||||||
|
.iter()
|
||||||
|
.any(|name| dns_name_matches_domain(name, &cached.domain));
|
||||||
|
}
|
||||||
|
cert_info
|
||||||
|
.subject_cn
|
||||||
|
.as_deref()
|
||||||
|
.map_or(true, |name| dns_name_matches_domain(name, &cached.domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dns_name_matches_domain(pattern: &str, domain: &str) -> bool {
|
||||||
|
let pattern = normalize_dns_name(pattern);
|
||||||
|
let domain = normalize_dns_name(domain);
|
||||||
|
if pattern == domain {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(suffix) = pattern.strip_prefix("*.") else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(prefix) = domain.strip_suffix(suffix) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
prefix.ends_with('.') && !prefix[..prefix.len() - 1].contains('.')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_dns_name(value: &str) -> String {
|
||||||
|
value.trim().trim_end_matches('.').to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn cached_with_cert_info(
|
||||||
|
domain: &str,
|
||||||
|
subject_cn: Option<&str>,
|
||||||
|
san_names: Vec<&str>,
|
||||||
|
) -> CachedTlsData {
|
||||||
|
CachedTlsData {
|
||||||
|
server_hello_template: ParsedServerHello {
|
||||||
|
version: [0x03, 0x03],
|
||||||
|
random: [0u8; 32],
|
||||||
|
session_id: Vec::new(),
|
||||||
|
cipher_suite: [0x13, 0x01],
|
||||||
|
compression: 0,
|
||||||
|
extensions: Vec::new(),
|
||||||
|
},
|
||||||
|
cert_info: Some(crate::tls_front::types::ParsedCertificateInfo {
|
||||||
|
not_after_unix: None,
|
||||||
|
not_before_unix: None,
|
||||||
|
issuer_cn: None,
|
||||||
|
subject_cn: subject_cn.map(str::to_string),
|
||||||
|
san_names: san_names.into_iter().map(str::to_string).collect(),
|
||||||
|
}),
|
||||||
|
cert_payload: None,
|
||||||
|
app_data_records_sizes: vec![1024],
|
||||||
|
total_app_data_len: 1024,
|
||||||
|
behavior_profile: TlsBehaviorProfile::default(),
|
||||||
|
fetched_at: SystemTime::now(),
|
||||||
|
domain: domain.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cert_info_domain_match_accepts_exact_san() {
|
||||||
|
let cached = cached_with_cert_info("b.com", Some("a.com"), vec!["b.com"]);
|
||||||
|
assert!(cert_info_matches_domain(&cached));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cert_info_domain_match_rejects_wrong_san() {
|
||||||
|
let cached = cached_with_cert_info("b.com", Some("b.com"), vec!["a.com"]);
|
||||||
|
assert!(!cert_info_matches_domain(&cached));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cert_info_domain_match_accepts_single_label_wildcard_san() {
|
||||||
|
let cached = cached_with_cert_info("api.b.com", None, vec!["*.b.com"]);
|
||||||
|
assert!(cert_info_matches_domain(&cached));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cert_info_domain_match_rejects_multi_label_wildcard_san() {
|
||||||
|
let cached = cached_with_cert_info("deep.api.b.com", None, vec!["*.b.com"]);
|
||||||
|
assert!(!cert_info_matches_domain(&cached));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_take_full_cert_budget_for_ip_uses_ttl() {
|
async fn test_take_full_cert_budget_for_ip_uses_ttl() {
|
||||||
let cache = TlsFrontCache::new(&["example.com".to_string()], 1024, "tlsfront-test-cache");
|
let cache = TlsFrontCache::new(&["example.com".to_string()], 1024, "tlsfront-test-cache");
|
||||||
|
|||||||
Reference in New Issue
Block a user