Split oversized runtime modules + Async tests hardened

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-30 09:39:25 +03:00
parent 281f63f940
commit 66f2b8889f
154 changed files with 25561 additions and 24915 deletions
+61
View File
@@ -0,0 +1,61 @@
use std::path::Path;
use tokio::io::AsyncReadExt;
use super::*;
pub(super) async fn read_disk_entry_bounded(path: &Path) -> std::io::Result<Vec<u8>> {
let file = tokio::fs::File::open(path).await?;
if file.metadata().await?.len() > TLS_FRONT_DISK_ENTRY_MAX_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"TLS cache entry exceeds the 1 MiB limit",
));
}
let mut bytes = Vec::new();
file.take(TLS_FRONT_DISK_ENTRY_MAX_BYTES.saturating_add(1))
.read_to_end(&mut bytes)
.await?;
if bytes.len() as u64 > TLS_FRONT_DISK_ENTRY_MAX_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"TLS cache entry grew beyond the 1 MiB limit while reading",
));
}
Ok(bytes)
}
pub(super) 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))
}
pub(super) 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('.')
}
pub(super) fn normalize_dns_name(value: &str) -> String {
value.trim().trim_end_matches('.').to_ascii_lowercase()
}
+378
View File
@@ -0,0 +1,378 @@
use std::path::Path;
use tokio::time::sleep;
use tracing::{debug, info, warn};
use super::*;
#[allow(dead_code)]
impl TlsFrontCache {
pub fn new(domains: &[String], default_len: usize, disk_path: impl AsRef<Path>) -> Self {
Self::new_with_full_cert_budget(
domains,
default_len,
disk_path,
Arc::new(TlsFullCertBudget::new()),
)
}
/// Creates a generation-local cache backed by the process-owned full-cert budget.
pub(crate) fn new_with_full_cert_budget(
domains: &[String],
default_len: usize,
disk_path: impl AsRef<Path>,
full_cert_budget: Arc<TlsFullCertBudget>,
) -> Self {
let default_template = ParsedServerHello {
version: [0x03, 0x03],
random: [0u8; 32],
session_id: Vec::new(),
cipher_suite: [0x13, 0x01],
compression: 0,
extensions: Vec::new(),
};
let default = Arc::new(CachedTlsData {
server_hello_template: default_template,
cert_info: None,
cert_payload: None,
app_data_records_sizes: vec![default_len],
total_app_data_len: default_len,
behavior_profile: TlsBehaviorProfile::default(),
fetched_at: SystemTime::now(),
domain: "default".to_string(),
});
let mut map = HashMap::new();
let mut full_cert_domain_keys = HashMap::new();
let mut disk_entry_names = HashSet::new();
for d in domains {
map.insert(d.clone(), default.clone());
disk_entry_names.insert(format!("{}.json", d.replace(['/', '\\'], "_")));
let canonical: Arc<str> = Arc::from(normalize_dns_name(d));
full_cert_domain_keys.insert(d.clone(), canonical.clone());
full_cert_domain_keys
.entry(canonical.to_string())
.or_insert(canonical);
}
Self {
memory: RwLock::new(map),
default,
full_cert_budget,
full_cert_domain_keys,
disk_entry_names,
disk_path: disk_path.as_ref().to_path_buf(),
}
}
pub async fn get(&self, sni: &str) -> Arc<CachedTlsData> {
let guard = self.memory.read().await;
guard
.get(sni)
.cloned()
.unwrap_or_else(|| self.default.clone())
}
pub async fn contains_domain(&self, domain: &str) -> bool {
self.memory.read().await.contains_key(domain)
}
pub(crate) async fn profile_health_snapshot(
&self,
domains: &[String],
max_domains: usize,
) -> (Vec<TlsFrontProfileHealth>, usize) {
let guard = self.memory.read().await;
let now = SystemTime::now();
let mut snapshot = Vec::with_capacity(domains.len().min(max_domains));
let mut suppressed = 0usize;
for domain in domains {
if snapshot.len() >= max_domains {
suppressed = suppressed.saturating_add(1);
continue;
}
let cached = guard
.get(domain)
.cloned()
.unwrap_or_else(|| self.default.clone());
let mut behavior = cached.behavior_profile.clone();
behavior.refresh_server_hello_summary(&cached.server_hello_template);
let age_seconds = now
.duration_since(cached.fetched_at)
.map(|duration| duration.as_secs())
.unwrap_or(0);
snapshot.push(TlsFrontProfileHealth {
domain: domain.clone(),
source: profile_source_label(behavior.source),
quality: profile_quality_label(behavior.quality),
key_share_group: key_share_group_label(behavior.server_hello_key_share_group),
age_seconds,
is_default: cached.domain == "default",
has_cert_info: cached.cert_info.is_some(),
has_cert_payload: cached.cert_payload.is_some(),
server_hello_record_len: behavior.server_hello_record_len,
server_hello_extensions: behavior.server_hello_extension_types.len(),
app_data_records: cached
.app_data_records_sizes
.len()
.max(behavior.app_data_record_sizes.len()),
ticket_records: behavior.ticket_record_sizes.len(),
change_cipher_spec_count: behavior.change_cipher_spec_count,
total_app_data_len: cached.total_app_data_len,
});
}
(snapshot, suppressed)
}
/// Returns configured domains that still resolve to the synthetic default profile.
pub(crate) async fn default_profile_domains(&self, domains: &[String]) -> Vec<String> {
let guard = self.memory.read().await;
domains
.iter()
.filter(|domain| {
guard.get(domain.as_str()).unwrap_or(&self.default).domain == "default"
})
.cloned()
.collect()
}
pub(super) fn full_cert_domain_key(&self, domain: &str) -> Arc<str> {
self.full_cert_domain_keys
.get(domain)
.cloned()
.unwrap_or_else(|| Arc::from(normalize_dns_name(domain)))
}
/// Returns true when the selected domain and client IP may receive a full cert payload.
pub async fn take_full_cert_budget_for_ip(
&self,
domain: &str,
client_ip: IpAddr,
ttl: Duration,
) -> bool {
self.full_cert_budget
.take(self.full_cert_domain_key(domain), client_ip, ttl)
.await
}
/// Returns the current process-owned full-cert budget entry count.
pub(crate) fn full_cert_budget_entries_for_metrics(&self) -> u64 {
self.full_cert_budget.entries_for_metrics()
}
/// Returns the cumulative process-owned full-cert budget cap drops.
pub(crate) fn full_cert_budget_cap_drops_for_metrics(&self) -> u64 {
self.full_cert_budget.cap_drops_for_metrics()
}
#[cfg(test)]
pub(super) async fn insert_full_cert_sent_for_tests(
&self,
domain: &str,
client_ip: IpAddr,
expires_at: Instant,
) {
let key = FullCertBudgetKey {
domain: self.full_cert_domain_key(domain),
client_ip,
};
let shard_index = self.full_cert_budget.shard_index(&key);
let mut guard = self.full_cert_budget.shards[shard_index].write().await;
if guard
.insert(
key,
FullCertBudgetEntry {
expires_at: Some(expires_at),
},
)
.is_none()
{
self.full_cert_budget
.entries
.fetch_add(1, Ordering::Relaxed);
}
}
#[cfg(test)]
pub(super) async fn full_cert_sent_is_empty_for_tests(&self) -> bool {
for shard in &self.full_cert_budget.shards {
if !shard.read().await.is_empty() {
return false;
}
}
true
}
#[cfg(test)]
pub(super) async fn full_cert_sent_contains_for_tests(
&self,
domain: &str,
client_ip: IpAddr,
) -> bool {
let key = FullCertBudgetKey {
domain: self.full_cert_domain_key(domain),
client_ip,
};
let shard_index = self.full_cert_budget.shard_index(&key);
self.full_cert_budget.shards[shard_index]
.read()
.await
.contains_key(&key)
}
pub async fn set(&self, domain: &str, data: CachedTlsData) {
let mut guard = self.memory.write().await;
guard.insert(domain.to_string(), Arc::new(data));
}
pub async fn load_from_disk(&self) {
let path = self.disk_path.clone();
if tokio::fs::create_dir_all(&path).await.is_err() {
return;
}
let mut loaded = 0usize;
for name in &self.disk_entry_names {
let entry_path = path.join(name);
let Ok(metadata) = tokio::fs::symlink_metadata(&entry_path).await else {
continue;
};
if !metadata.file_type().is_file() {
continue;
}
if let Ok(data) = read_disk_entry_bounded(&entry_path).await
&& let Ok(mut cached) = serde_json::from_slice::<CachedTlsData>(&data)
{
if cached.domain.is_empty()
|| cached.domain.len() > 255
|| !cached
.domain
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
{
warn!(file = %name, "Skipping TLS cache entry with invalid domain");
continue;
}
if !self.full_cert_domain_keys.contains_key(&cached.domain) {
warn!(
file = %name,
domain = %cached.domain,
"Skipping TLS cache entry outside configured domains"
);
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.
if let Ok(modified) = metadata.modified() {
cached.fetched_at = modified;
}
// Drop entries older than 72h
if let Ok(age) = cached.fetched_at.elapsed()
&& age > Duration::from_secs(72 * 3600)
{
warn!(domain = %cached.domain, "Skipping stale TLS cache entry (>72h)");
continue;
}
cached
.behavior_profile
.refresh_server_hello_summary(&cached.server_hello_template);
let domain = cached.domain.clone();
self.set(&domain, cached).await;
loaded += 1;
}
}
if loaded > 0 {
info!(count = loaded, "Loaded TLS cache entries from disk");
}
}
async fn persist(&self, domain: &str, data: &CachedTlsData) {
if tokio::fs::create_dir_all(&self.disk_path).await.is_err() {
return;
}
let fname = format!("{}.json", domain.replace(['/', '\\'], "_"));
let path = self.disk_path.join(fname);
if let Ok(json) = serde_json::to_vec_pretty(data) {
if json.len() as u64 > TLS_FRONT_DISK_ENTRY_MAX_BYTES {
warn!(
domain,
bytes = json.len(),
"Skipping oversized TLS cache persistence"
);
return;
}
// best-effort write
let _ = tokio::fs::write(path, json).await;
}
}
/// Spawn background updater that periodically refreshes cached domains using provided fetcher.
pub fn spawn_updater<F>(self: Arc<Self>, domains: Vec<String>, interval: Duration, fetcher: F)
where
F: Fn(String) -> tokio::task::JoinHandle<()> + Send + Sync + 'static,
{
tokio::spawn(async move {
loop {
for domain in &domains {
let _ = fetcher(domain.clone()).await;
}
sleep(interval).await;
}
});
}
/// Replace cached entry from a fetch result.
pub async fn update_from_fetch(&self, domain: &str, fetched: TlsFetchResult) {
let TlsFetchResult {
server_hello_parsed,
app_data_records_sizes,
total_app_data_len,
mut behavior_profile,
cert_info,
cert_payload,
} = fetched;
behavior_profile.refresh_server_hello_summary(&server_hello_parsed);
let quality = behavior_profile.quality;
let data = CachedTlsData {
server_hello_template: server_hello_parsed,
cert_info,
cert_payload,
app_data_records_sizes: app_data_records_sizes.clone(),
total_app_data_len,
behavior_profile,
fetched_at: SystemTime::now(),
domain: domain.to_string(),
};
self.set(domain, data.clone()).await;
self.persist(domain, &data).await;
if quality == TlsProfileQuality::RawStrict {
debug!(domain = %domain, len = total_app_data_len, "TLS cache updated");
} else {
warn!(
domain = %domain,
quality = profile_quality_label(quality),
len = total_app_data_len,
"TLS cache updated with non-strict front profile"
);
}
}
pub fn default_entry(&self) -> Arc<CachedTlsData> {
self.default.clone()
}
pub fn disk_path(&self) -> &Path {
&self.disk_path
}
}
+294
View File
@@ -0,0 +1,294 @@
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]
async fn default_profile_domains_reports_only_unprepared_entries() {
let domains = vec!["ready.example".to_string(), "pending.example".to_string()];
let cache = TlsFrontCache::new(&domains, 1024, "tlsfront-test-cache");
cache
.set(
"ready.example",
cached_with_cert_info("ready.example", None, Vec::new()),
)
.await;
assert_eq!(
cache.default_profile_domains(&domains).await,
vec!["pending.example".to_string()]
);
}
#[tokio::test]
async fn test_take_full_cert_budget_for_ip_uses_ttl() {
let cache = TlsFrontCache::new(&["example.com".to_string()], 1024, "tlsfront-test-cache");
let ip: IpAddr = "127.0.0.1".parse().expect("ip");
let ttl = Duration::from_millis(80);
assert!(
cache
.take_full_cert_budget_for_ip("example.com", ip, ttl)
.await
);
assert!(
!cache
.take_full_cert_budget_for_ip("example.com", ip, ttl)
.await
);
tokio::time::sleep(Duration::from_millis(90)).await;
assert!(
cache
.take_full_cert_budget_for_ip("example.com", ip, ttl)
.await
);
}
#[tokio::test]
async fn test_take_full_cert_budget_for_ip_zero_ttl_always_allows_full_payload() {
let cache = TlsFrontCache::new(&["example.com".to_string()], 1024, "tlsfront-test-cache");
let ttl = Duration::ZERO;
for idx in 0..100_000u32 {
let ip = IpAddr::V4(std::net::Ipv4Addr::new(
10,
((idx >> 16) & 0xff) as u8,
((idx >> 8) & 0xff) as u8,
(idx & 0xff) as u8,
));
assert!(
cache
.take_full_cert_budget_for_ip("example.com", ip, ttl)
.await
);
}
assert!(cache.full_cert_sent_is_empty_for_tests().await);
}
#[tokio::test]
async fn test_take_full_cert_budget_for_ip_sweeps_expired_entries_when_due() {
let cache = TlsFrontCache::new(&["example.com".to_string()], 1024, "tlsfront-test-cache");
let stale_ip: IpAddr = "127.0.0.1".parse().expect("ip");
let new_ip: IpAddr = "127.0.0.2".parse().expect("ip");
let ttl = Duration::from_secs(1);
let stale_expires_at = Instant::now()
.checked_sub(Duration::from_secs(1))
.unwrap_or_else(Instant::now);
cache
.insert_full_cert_sent_for_tests("example.com", stale_ip, stale_expires_at)
.await;
let stale_key = FullCertBudgetKey {
domain: cache.full_cert_domain_key("example.com"),
client_ip: stale_ip,
};
cache.full_cert_budget.sweep_cursor.store(
cache.full_cert_budget.shard_index(&stale_key),
Ordering::Relaxed,
);
cache
.full_cert_budget
.last_sweep_epoch_secs
.store(0, Ordering::Relaxed);
assert!(
cache
.take_full_cert_budget_for_ip("example.com", new_ip, ttl)
.await
);
assert!(
!cache
.full_cert_sent_contains_for_tests("example.com", stale_ip)
.await
);
assert!(
cache
.full_cert_sent_contains_for_tests("example.com", new_ip)
.await
);
}
#[tokio::test]
async fn test_take_full_cert_budget_for_ip_does_not_sweep_every_call() {
let cache = TlsFrontCache::new(&["example.com".to_string()], 1024, "tlsfront-test-cache");
let stale_ip: IpAddr = "127.0.0.1".parse().expect("ip");
let new_ip: IpAddr = "127.0.0.2".parse().expect("ip");
let ttl = Duration::from_secs(1);
let stale_expires_at = Instant::now()
.checked_sub(Duration::from_secs(1))
.unwrap_or_else(Instant::now);
let now_epoch_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
cache
.insert_full_cert_sent_for_tests("example.com", stale_ip, stale_expires_at)
.await;
cache
.full_cert_budget
.last_sweep_epoch_secs
.store(now_epoch_secs, Ordering::Relaxed);
assert!(
cache
.take_full_cert_budget_for_ip("example.com", new_ip, ttl)
.await
);
assert!(
cache
.full_cert_sent_contains_for_tests("example.com", stale_ip)
.await
);
assert!(
cache
.full_cert_sent_contains_for_tests("example.com", new_ip)
.await
);
}
#[tokio::test]
async fn full_cert_budget_is_shared_across_cache_generations_and_scoped_by_domain() {
let budget = Arc::new(TlsFullCertBudget::new());
let domains = ["one.example".to_string(), "two.example".to_string()];
let first = TlsFrontCache::new_with_full_cert_budget(
&domains,
1024,
"tlsfront-test-cache",
budget.clone(),
);
let second =
TlsFrontCache::new_with_full_cert_budget(&domains, 1024, "tlsfront-test-cache", budget);
let ip: IpAddr = "127.0.0.1".parse().expect("ip");
let ttl = Duration::from_secs(60);
assert!(
first
.take_full_cert_budget_for_ip("one.example", ip, ttl)
.await
);
assert!(
!second
.take_full_cert_budget_for_ip("one.example", ip, ttl)
.await
);
assert!(
second
.take_full_cert_budget_for_ip("two.example", ip, ttl)
.await
);
assert_eq!(second.full_cert_budget_entries_for_metrics(), 2);
}
#[tokio::test]
async fn existing_full_cert_entry_keeps_its_own_expiry_after_ttl_change() {
let cache = TlsFrontCache::new(&["example.com".to_string()], 1024, "tlsfront-test-cache");
let ip: IpAddr = "127.0.0.1".parse().expect("ip");
assert!(
cache
.take_full_cert_budget_for_ip("example.com", ip, Duration::from_millis(80))
.await
);
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(
!cache
.take_full_cert_budget_for_ip("example.com", ip, Duration::from_millis(1))
.await
);
tokio::time::sleep(Duration::from_millis(70)).await;
assert!(
cache
.take_full_cert_budget_for_ip("example.com", ip, Duration::from_millis(1))
.await
);
}
#[tokio::test]
async fn disk_reader_rejects_an_entry_above_the_hard_limit() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("oversized.json");
tokio::fs::write(
&path,
vec![0u8; TLS_FRONT_DISK_ENTRY_MAX_BYTES as usize + 1],
)
.await
.unwrap();
let error = read_disk_entry_bounded(&path).await.unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
}
#[cfg(unix)]
#[tokio::test]
async fn disk_loader_does_not_follow_a_configured_name_symlink() {
let directory = tempfile::tempdir().unwrap();
let target = directory.path().join("outside.json");
let cached = cached_with_cert_info("example.com", None, Vec::new());
tokio::fs::write(&target, serde_json::to_vec(&cached).unwrap())
.await
.unwrap();
std::os::unix::fs::symlink(&target, directory.path().join("example.com.json")).unwrap();
let cache = TlsFrontCache::new(&["example.com".to_string()], 1024, directory.path());
cache.load_from_disk().await;
assert_eq!(cache.get("example.com").await.domain, "default");
}