mirror of
https://github.com/telemt/telemt.git
synced 2026-04-30 17:04:11 +03:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1c947e8e3 | ||
|
|
cfe01dced2 | ||
|
|
8520955a5f | ||
|
|
065786b839 | ||
|
|
f0e1a6cf1c | ||
|
|
236bbb4970 | ||
|
|
8ef5263fce | ||
|
|
893cef22e3 | ||
|
|
bdfa641843 | ||
|
|
007fc86189 | ||
|
|
10c9bcd97d | ||
|
|
8ab9405dca | ||
|
|
9412f089c0 | ||
|
|
d567dfe40b |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2791,7 +2791,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.4.7"
|
version = "3.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.4.7"
|
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
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pub(super) async fn load_config_from_disk(config_path: &Path) -> Result<ProxyCon
|
|||||||
.map_err(|e| ApiFailure::internal(format!("failed to load config: {}", e)))
|
.map_err(|e| ApiFailure::internal(format!("failed to load config: {}", e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(super) async fn save_config_to_disk(
|
pub(super) async fn save_config_to_disk(
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
cfg: &ProxyConfig,
|
cfg: &ProxyConfig,
|
||||||
@@ -106,6 +107,12 @@ pub(super) async fn save_access_sections_to_disk(
|
|||||||
if applied.contains(section) {
|
if applied.contains(section) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if find_toml_table_bounds(&content, section.table_name()).is_none()
|
||||||
|
&& access_section_is_empty(cfg, *section)
|
||||||
|
{
|
||||||
|
applied.push(*section);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let rendered = render_access_section(cfg, *section)?;
|
let rendered = render_access_section(cfg, *section)?;
|
||||||
content = upsert_toml_table(&content, section.table_name(), &rendered);
|
content = upsert_toml_table(&content, section.table_name(), &rendered);
|
||||||
applied.push(*section);
|
applied.push(*section);
|
||||||
@@ -183,6 +190,17 @@ fn render_access_section(cfg: &ProxyConfig, section: AccessSection) -> Result<St
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn access_section_is_empty(cfg: &ProxyConfig, section: AccessSection) -> bool {
|
||||||
|
match section {
|
||||||
|
AccessSection::Users => cfg.access.users.is_empty(),
|
||||||
|
AccessSection::UserAdTags => cfg.access.user_ad_tags.is_empty(),
|
||||||
|
AccessSection::UserMaxTcpConns => cfg.access.user_max_tcp_conns.is_empty(),
|
||||||
|
AccessSection::UserExpirations => cfg.access.user_expirations.is_empty(),
|
||||||
|
AccessSection::UserDataQuota => cfg.access.user_data_quota.is_empty(),
|
||||||
|
AccessSection::UserMaxUniqueIps => cfg.access.user_max_unique_ips.is_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize_table_body<T: Serialize>(value: &T) -> Result<String, ApiFailure> {
|
fn serialize_table_body<T: Serialize>(value: &T) -> Result<String, ApiFailure> {
|
||||||
toml::to_string(value)
|
toml::to_string(value)
|
||||||
.map_err(|e| ApiFailure::internal(format!("failed to serialize access section: {}", e)))
|
.map_err(|e| ApiFailure::internal(format!("failed to serialize access section: {}", e)))
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
143
src/api/users.rs
143
src/api/users.rs
@@ -8,12 +8,12 @@ use crate::stats::Stats;
|
|||||||
|
|
||||||
use super::ApiShared;
|
use super::ApiShared;
|
||||||
use super::config_store::{
|
use super::config_store::{
|
||||||
AccessSection, ensure_expected_revision, load_config_from_disk, save_access_sections_to_disk,
|
AccessSection, current_revision, ensure_expected_revision, load_config_from_disk,
|
||||||
save_config_to_disk,
|
save_access_sections_to_disk,
|
||||||
};
|
};
|
||||||
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;
|
||||||
@@ -176,6 +176,13 @@ pub(super) async fn patch_user(
|
|||||||
expected_revision: Option<String>,
|
expected_revision: Option<String>,
|
||||||
shared: &ApiShared,
|
shared: &ApiShared,
|
||||||
) -> Result<(UserInfo, String), ApiFailure> {
|
) -> Result<(UserInfo, String), ApiFailure> {
|
||||||
|
let touches_users = body.secret.is_some();
|
||||||
|
let touches_user_ad_tags = !matches!(&body.user_ad_tag, Patch::Unchanged);
|
||||||
|
let touches_user_max_tcp_conns = !matches!(&body.max_tcp_conns, Patch::Unchanged);
|
||||||
|
let touches_user_expirations = !matches!(&body.expiration_rfc3339, Patch::Unchanged);
|
||||||
|
let touches_user_data_quota = !matches!(&body.data_quota_bytes, Patch::Unchanged);
|
||||||
|
let touches_user_max_unique_ips = !matches!(&body.max_unique_ips, Patch::Unchanged);
|
||||||
|
|
||||||
if let Some(secret) = body.secret.as_ref()
|
if let Some(secret) = body.secret.as_ref()
|
||||||
&& !is_valid_user_secret(secret)
|
&& !is_valid_user_secret(secret)
|
||||||
{
|
{
|
||||||
@@ -265,7 +272,31 @@ pub(super) async fn patch_user(
|
|||||||
cfg.validate()
|
cfg.validate()
|
||||||
.map_err(|e| ApiFailure::bad_request(format!("config validation failed: {}", e)))?;
|
.map_err(|e| ApiFailure::bad_request(format!("config validation failed: {}", e)))?;
|
||||||
|
|
||||||
let revision = save_config_to_disk(&shared.config_path, &cfg).await?;
|
let mut touched_sections = Vec::new();
|
||||||
|
if touches_users {
|
||||||
|
touched_sections.push(AccessSection::Users);
|
||||||
|
}
|
||||||
|
if touches_user_ad_tags {
|
||||||
|
touched_sections.push(AccessSection::UserAdTags);
|
||||||
|
}
|
||||||
|
if touches_user_max_tcp_conns {
|
||||||
|
touched_sections.push(AccessSection::UserMaxTcpConns);
|
||||||
|
}
|
||||||
|
if touches_user_expirations {
|
||||||
|
touched_sections.push(AccessSection::UserExpirations);
|
||||||
|
}
|
||||||
|
if touches_user_data_quota {
|
||||||
|
touched_sections.push(AccessSection::UserDataQuota);
|
||||||
|
}
|
||||||
|
if touches_user_max_unique_ips {
|
||||||
|
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
||||||
|
}
|
||||||
|
|
||||||
|
let revision = if touched_sections.is_empty() {
|
||||||
|
current_revision(&shared.config_path).await?
|
||||||
|
} else {
|
||||||
|
save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?
|
||||||
|
};
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
match max_unique_ips_change {
|
match max_unique_ips_change {
|
||||||
Some(Some(limit)) => shared.ip_tracker.set_user_limit(user, limit).await,
|
Some(Some(limit)) => shared.ip_tracker.set_user_limit(user, limit).await,
|
||||||
@@ -438,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
|
||||||
@@ -492,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 {
|
||||||
@@ -518,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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,6 +570,7 @@ fn build_user_links(
|
|||||||
classic,
|
classic,
|
||||||
secure,
|
secure,
|
||||||
tls,
|
tls,
|
||||||
|
tls_domains: tls_domain_links,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,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::*;
|
||||||
@@ -730,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")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ pub(crate) fn default_fake_cert_len() -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_tls_front_dir() -> String {
|
pub(crate) fn default_tls_front_dir() -> String {
|
||||||
"/etc/telemt/tlsfront".to_string()
|
"tlsfront".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_replay_check_len() -> usize {
|
pub(crate) fn default_replay_check_len() -> usize {
|
||||||
@@ -568,7 +568,7 @@ pub(crate) fn default_beobachten_flush_secs() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_beobachten_file() -> String {
|
pub(crate) fn default_beobachten_file() -> String {
|
||||||
"/etc/telemt/beobachten.txt".to_string()
|
"beobachten.txt".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_tls_new_session_tickets() -> u8 {
|
pub(crate) fn default_tls_new_session_tickets() -> u8 {
|
||||||
|
|||||||
@@ -278,9 +278,11 @@ impl UserIpTracker {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let is_new_ip = !user_recent.contains_key(&ip);
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
if let Some(limit) = limit {
|
||||||
let active_limit_reached = user_active.len() >= limit;
|
let active_limit_reached = user_active.len() >= limit;
|
||||||
let recent_limit_reached = user_recent.len() >= limit;
|
let recent_limit_reached = user_recent.len() >= limit && is_new_ip;
|
||||||
let deny = match mode {
|
let deny = match mode {
|
||||||
UserMaxUniqueIpsMode::ActiveWindow => active_limit_reached,
|
UserMaxUniqueIpsMode::ActiveWindow => active_limit_reached,
|
||||||
UserMaxUniqueIpsMode::TimeWindow => recent_limit_reached,
|
UserMaxUniqueIpsMode::TimeWindow => recent_limit_reached,
|
||||||
@@ -860,4 +862,19 @@ mod tests {
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
assert!(!stale_exists);
|
assert!(!stale_exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_time_window_allows_same_ip_reconnect() {
|
||||||
|
let tracker = UserIpTracker::new();
|
||||||
|
tracker.set_user_limit("test_user", 1).await;
|
||||||
|
tracker
|
||||||
|
.set_limit_policy(UserMaxUniqueIpsMode::TimeWindow, 1)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let ip1 = test_ipv4(10, 4, 0, 1);
|
||||||
|
|
||||||
|
assert!(tracker.check_and_add("test_user", ip1).await.is_ok());
|
||||||
|
tracker.remove_ip("test_user", ip1).await;
|
||||||
|
assert!(tracker.check_and_add("test_user", ip1).await.is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::items_after_test_module)]
|
#![allow(clippy::items_after_test_module)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
@@ -17,7 +17,7 @@ use crate::transport::middle_proxy::{
|
|||||||
|
|
||||||
pub(crate) fn resolve_runtime_config_path(
|
pub(crate) fn resolve_runtime_config_path(
|
||||||
config_path_cli: &str,
|
config_path_cli: &str,
|
||||||
startup_cwd: &std::path::Path,
|
startup_cwd: &Path,
|
||||||
config_path_explicit: bool,
|
config_path_explicit: bool,
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
if config_path_explicit {
|
if config_path_explicit {
|
||||||
@@ -46,6 +46,39 @@ pub(crate) fn resolve_runtime_config_path(
|
|||||||
startup_cwd.join("config.toml")
|
startup_cwd.join("config.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_runtime_base_dir(
|
||||||
|
config_path: &Path,
|
||||||
|
startup_cwd: &Path,
|
||||||
|
config_path_explicit: bool,
|
||||||
|
data_path: Option<&Path>,
|
||||||
|
) -> PathBuf {
|
||||||
|
if let Some(path) = data_path {
|
||||||
|
return normalize_runtime_dir(path, startup_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if startup_cwd != Path::new("/") {
|
||||||
|
return normalize_runtime_dir(startup_cwd, startup_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if config_path_explicit
|
||||||
|
&& let Some(parent) = config_path.parent()
|
||||||
|
&& !parent.as_os_str().is_empty()
|
||||||
|
{
|
||||||
|
return normalize_runtime_dir(parent, startup_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathBuf::from("/etc/telemt")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_runtime_dir(path: &Path, startup_cwd: &Path) -> PathBuf {
|
||||||
|
let absolute = if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
startup_cwd.join(path)
|
||||||
|
};
|
||||||
|
absolute.canonicalize().unwrap_or(absolute)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parsed CLI arguments.
|
/// Parsed CLI arguments.
|
||||||
pub(crate) struct CliArgs {
|
pub(crate) struct CliArgs {
|
||||||
pub config_path: String,
|
pub config_path: String,
|
||||||
@@ -231,9 +264,11 @@ fn print_help() {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
expected_handshake_close_description, is_expected_handshake_eof, peer_close_description,
|
expected_handshake_close_description, is_expected_handshake_eof, peer_close_description,
|
||||||
resolve_runtime_config_path,
|
resolve_runtime_base_dir, resolve_runtime_config_path,
|
||||||
};
|
};
|
||||||
use crate::error::{ProxyError, StreamError};
|
use crate::error::{ProxyError, StreamError};
|
||||||
|
|
||||||
@@ -304,6 +339,91 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir(&startup_cwd);
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_base_dir_prefers_cli_data_path() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let startup_cwd = std::env::temp_dir().join(format!("telemt_runtime_base_cwd_{nonce}"));
|
||||||
|
let data_path = std::env::temp_dir().join(format!("telemt_runtime_base_data_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
std::fs::create_dir_all(&data_path).unwrap();
|
||||||
|
|
||||||
|
let resolved = resolve_runtime_base_dir(
|
||||||
|
&startup_cwd.join("config.toml"),
|
||||||
|
&startup_cwd,
|
||||||
|
true,
|
||||||
|
Some(&data_path),
|
||||||
|
);
|
||||||
|
assert_eq!(resolved, data_path.canonicalize().unwrap());
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir(&data_path);
|
||||||
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_base_dir_uses_working_directory_before_explicit_config_parent() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let startup_cwd = std::env::temp_dir().join(format!("telemt_runtime_base_start_{nonce}"));
|
||||||
|
let config_dir = std::env::temp_dir().join(format!("telemt_runtime_base_cfg_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
std::fs::create_dir_all(&config_dir).unwrap();
|
||||||
|
|
||||||
|
let resolved =
|
||||||
|
resolve_runtime_base_dir(&config_dir.join("telemt.toml"), &startup_cwd, true, None);
|
||||||
|
assert_eq!(resolved, startup_cwd.canonicalize().unwrap());
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir(&config_dir);
|
||||||
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_base_dir_uses_explicit_config_parent_from_root() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let config_dir = std::env::temp_dir().join(format!("telemt_runtime_base_root_cfg_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&config_dir).unwrap();
|
||||||
|
|
||||||
|
let resolved =
|
||||||
|
resolve_runtime_base_dir(&config_dir.join("telemt.toml"), Path::new("/"), true, None);
|
||||||
|
assert_eq!(resolved, config_dir.canonicalize().unwrap());
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir(&config_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_base_dir_uses_systemd_working_directory_before_etc() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let startup_cwd = std::env::temp_dir().join(format!("telemt_runtime_base_systemd_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
|
||||||
|
let resolved =
|
||||||
|
resolve_runtime_base_dir(&startup_cwd.join("config.toml"), &startup_cwd, false, None);
|
||||||
|
assert_eq!(resolved, startup_cwd.canonicalize().unwrap());
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_base_dir_falls_back_to_etc_from_root() {
|
||||||
|
let resolved = resolve_runtime_base_dir(
|
||||||
|
Path::new("/etc/telemt/config.toml"),
|
||||||
|
Path::new("/"),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(resolved, PathBuf::from("/etc/telemt"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn expected_handshake_eof_matches_connection_reset() {
|
fn expected_handshake_eof_matches_connection_reset() {
|
||||||
let err = ProxyError::Io(std::io::Error::from(std::io::ErrorKind::ConnectionReset));
|
let err = ProxyError::Io(std::io::Error::from(std::io::ErrorKind::ConnectionReset));
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ use crate::stats::{ReplayChecker, Stats};
|
|||||||
use crate::stream::BufferPool;
|
use crate::stream::BufferPool;
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
use helpers::{parse_cli, resolve_runtime_config_path};
|
use helpers::{parse_cli, resolve_runtime_base_dir, resolve_runtime_config_path};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use crate::daemon::{DaemonOptions, PidFile, drop_privileges};
|
use crate::daemon::{DaemonOptions, PidFile, drop_privileges};
|
||||||
@@ -112,8 +112,51 @@ async fn run_telemt_core(
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if let Some(ref data_path) = data_path
|
||||||
|
&& !data_path.is_absolute()
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] data_path must be absolute: {}",
|
||||||
|
data_path.display()
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
let mut config_path =
|
let mut config_path =
|
||||||
resolve_runtime_config_path(&config_path_cli, &startup_cwd, config_path_explicit);
|
resolve_runtime_config_path(&config_path_cli, &startup_cwd, config_path_explicit);
|
||||||
|
let runtime_base_dir = resolve_runtime_base_dir(
|
||||||
|
&config_path,
|
||||||
|
&startup_cwd,
|
||||||
|
config_path_explicit,
|
||||||
|
data_path.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if !runtime_base_dir.exists()
|
||||||
|
&& let Err(e) = std::fs::create_dir_all(&runtime_base_dir)
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Can't create runtime directory {}: {}",
|
||||||
|
runtime_base_dir.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !runtime_base_dir.is_dir() {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Runtime path exists but is not a directory: {}",
|
||||||
|
runtime_base_dir.display()
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = std::env::set_current_dir(&runtime_base_dir) {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Can't use runtime directory {}: {}",
|
||||||
|
runtime_base_dir.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
let mut config = match ProxyConfig::load(&config_path) {
|
let mut config = match ProxyConfig::load(&config_path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -156,16 +199,15 @@ async fn run_telemt_core(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let system_dir = std::path::Path::new("/etc/telemt");
|
let runtime_config_path = runtime_base_dir.join("telemt.toml");
|
||||||
let system_config_path = system_dir.join("telemt.toml");
|
let fallback_config_path = runtime_base_dir.join("config.toml");
|
||||||
let startup_config_path = startup_cwd.join("config.toml");
|
|
||||||
let mut persisted = false;
|
let mut persisted = false;
|
||||||
|
|
||||||
if let Some(serialized) = serialized.as_ref() {
|
if let Some(serialized) = serialized.as_ref() {
|
||||||
match std::fs::create_dir_all(system_dir) {
|
match std::fs::create_dir_all(&runtime_base_dir) {
|
||||||
Ok(()) => match std::fs::write(&system_config_path, serialized) {
|
Ok(()) => match std::fs::write(&runtime_config_path, serialized) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
config_path = system_config_path;
|
config_path = runtime_config_path;
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[telemt] Created default config at {}",
|
"[telemt] Created default config at {}",
|
||||||
config_path.display()
|
config_path.display()
|
||||||
@@ -175,7 +217,7 @@ async fn run_telemt_core(
|
|||||||
Err(write_error) => {
|
Err(write_error) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[telemt] Warning: failed to write default config at {}: {}",
|
"[telemt] Warning: failed to write default config at {}: {}",
|
||||||
system_config_path.display(),
|
runtime_config_path.display(),
|
||||||
write_error
|
write_error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -183,16 +225,16 @@ async fn run_telemt_core(
|
|||||||
Err(create_error) => {
|
Err(create_error) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[telemt] Warning: failed to create {}: {}",
|
"[telemt] Warning: failed to create {}: {}",
|
||||||
system_dir.display(),
|
runtime_base_dir.display(),
|
||||||
create_error
|
create_error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !persisted {
|
if !persisted {
|
||||||
match std::fs::write(&startup_config_path, serialized) {
|
match std::fs::write(&fallback_config_path, serialized) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
config_path = startup_config_path;
|
config_path = fallback_config_path;
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[telemt] Created default config at {}",
|
"[telemt] Created default config at {}",
|
||||||
config_path.display()
|
config_path.display()
|
||||||
@@ -202,7 +244,7 @@ async fn run_telemt_core(
|
|||||||
Err(write_error) => {
|
Err(write_error) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[telemt] Warning: failed to write default config at {}: {}",
|
"[telemt] Warning: failed to write default config at {}: {}",
|
||||||
startup_config_path.display(),
|
fallback_config_path.display(),
|
||||||
write_error
|
write_error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1612,8 +1612,6 @@ impl RunningClientHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let tracks_ip = ip_tracker.get_user_limit(user).await.is_some();
|
|
||||||
if tracks_ip {
|
|
||||||
match ip_tracker.check_and_add(user, peer_addr.ip()).await {
|
match ip_tracker.check_and_add(user, peer_addr.ip()).await {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
@@ -1629,14 +1627,13 @@ impl RunningClientHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(UserConnectionReservation::new(
|
Ok(UserConnectionReservation::new(
|
||||||
stats,
|
stats,
|
||||||
ip_tracker,
|
ip_tracker,
|
||||||
user.to_string(),
|
user.to_string(),
|
||||||
peer_addr.ip(),
|
peer_addr.ip(),
|
||||||
tracks_ip,
|
true,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1679,7 +1676,6 @@ impl RunningClientHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip_tracker.get_user_limit(user).await.is_some() {
|
|
||||||
match ip_tracker.check_and_add(user, peer_addr.ip()).await {
|
match ip_tracker.check_and_add(user, peer_addr.ip()).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
ip_tracker.remove_ip(user, peer_addr.ip()).await;
|
ip_tracker.remove_ip(user, peer_addr.ip()).await;
|
||||||
@@ -1697,7 +1693,6 @@ impl RunningClientHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
stats.decrement_user_curr_connects(user);
|
stats.decrement_user_curr_connects(user);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -960,6 +960,36 @@ async fn reservation_limit_failure_does_not_leak_curr_connects_counter() {
|
|||||||
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0);
|
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unlimited_unique_ip_user_is_still_visible_in_active_ip_tracker() {
|
||||||
|
let user = "active-ip-observed-user";
|
||||||
|
let config = crate::config::ProxyConfig::default();
|
||||||
|
let stats = Arc::new(crate::stats::Stats::new());
|
||||||
|
let ip_tracker = Arc::new(crate::ip_tracker::UserIpTracker::new());
|
||||||
|
let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 200, 17)), 50017);
|
||||||
|
|
||||||
|
let reservation = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
|
user,
|
||||||
|
&config,
|
||||||
|
stats.clone(),
|
||||||
|
peer,
|
||||||
|
ip_tracker.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("reservation without unique-IP limit must succeed");
|
||||||
|
|
||||||
|
assert_eq!(stats.get_user_curr_connects(user), 1);
|
||||||
|
assert_eq!(
|
||||||
|
ip_tracker.get_active_ip_count(user).await,
|
||||||
|
1,
|
||||||
|
"active IP observability must not depend on unique-IP limit enforcement"
|
||||||
|
);
|
||||||
|
|
||||||
|
reservation.release().await;
|
||||||
|
assert_eq!(stats.get_user_curr_connects(user), 0);
|
||||||
|
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn short_tls_probe_is_masked_through_client_pipeline() {
|
async fn short_tls_probe_is_masked_through_client_pipeline() {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
|||||||
@@ -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