mirror of
https://github.com/telemt/telemt.git
synced 2026-06-19 02:00:08 +07:00
Expose TLS Fetcher Profile Quality for ServerHello fidelity
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
+60
-1
@@ -381,11 +381,32 @@ async fn render_tls_front_profile_health(
|
|||||||
"# HELP telemt_tls_front_profile_info TLS front profile source and feature flags per configured domain"
|
"# HELP telemt_tls_front_profile_info TLS front profile source and feature flags per configured domain"
|
||||||
);
|
);
|
||||||
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_info gauge");
|
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_info gauge");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_tls_front_profile_quality_info TLS front profile quality and key-share group per configured domain"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_quality_info gauge");
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"# HELP telemt_tls_front_profile_age_seconds Age of cached TLS front profile data per configured domain"
|
"# HELP telemt_tls_front_profile_age_seconds Age of cached TLS front profile data per configured domain"
|
||||||
);
|
);
|
||||||
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_age_seconds gauge");
|
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_age_seconds gauge");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_tls_front_profile_server_hello_bytes TLS front cached ServerHello record body bytes per configured domain"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_tls_front_profile_server_hello_bytes gauge"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_tls_front_profile_server_hello_extensions TLS front cached visible ServerHello extension count per configured domain"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_tls_front_profile_server_hello_extensions gauge"
|
||||||
|
);
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"# HELP telemt_tls_front_profile_app_data_records TLS front cached app-data record count per configured domain"
|
"# HELP telemt_tls_front_profile_app_data_records TLS front cached app-data record count per configured domain"
|
||||||
@@ -420,11 +441,26 @@ async fn render_tls_front_profile_health(
|
|||||||
"telemt_tls_front_profile_info{{domain=\"{}\",source=\"{}\",is_default=\"{}\",has_cert_info=\"{}\",has_cert_payload=\"{}\"}} 1",
|
"telemt_tls_front_profile_info{{domain=\"{}\",source=\"{}\",is_default=\"{}\",has_cert_info=\"{}\",has_cert_payload=\"{}\"}} 1",
|
||||||
domain, item.source, item.is_default, item.has_cert_info, item.has_cert_payload
|
domain, item.source, item.is_default, item.has_cert_info, item.has_cert_payload
|
||||||
);
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_tls_front_profile_quality_info{{domain=\"{}\",quality=\"{}\",key_share_group=\"{}\"}} 1",
|
||||||
|
domain, item.quality, item.key_share_group
|
||||||
|
);
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"telemt_tls_front_profile_age_seconds{{domain=\"{}\"}} {}",
|
"telemt_tls_front_profile_age_seconds{{domain=\"{}\"}} {}",
|
||||||
domain, item.age_seconds
|
domain, item.age_seconds
|
||||||
);
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_tls_front_profile_server_hello_bytes{{domain=\"{}\"}} {}",
|
||||||
|
domain, item.server_hello_record_len
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_tls_front_profile_server_hello_extensions{{domain=\"{}\"}} {}",
|
||||||
|
domain, item.server_hello_extensions
|
||||||
|
);
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"telemt_tls_front_profile_app_data_records{{domain=\"{}\"}} {}",
|
"telemt_tls_front_profile_app_data_records{{domain=\"{}\"}} {}",
|
||||||
@@ -3901,7 +3937,10 @@ mod tests {
|
|||||||
session_id: Vec::new(),
|
session_id: Vec::new(),
|
||||||
cipher_suite: [0x13, 0x01],
|
cipher_suite: [0x13, 0x01],
|
||||||
compression: 0,
|
compression: 0,
|
||||||
extensions: Vec::new(),
|
extensions: vec![crate::tls_front::types::TlsExtension {
|
||||||
|
ext_type: 0x0033,
|
||||||
|
data: vec![0x00, 0x1d, 0x00, 0x20],
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
cert_info: None,
|
cert_info: None,
|
||||||
cert_payload: Some(TlsCertPayload {
|
cert_payload: Some(TlsCertPayload {
|
||||||
@@ -3915,6 +3954,7 @@ mod tests {
|
|||||||
app_data_record_sizes: vec![1024, 512],
|
app_data_record_sizes: vec![1024, 512],
|
||||||
ticket_record_sizes: vec![69],
|
ticket_record_sizes: vec![69],
|
||||||
source: TlsProfileSource::Merged,
|
source: TlsProfileSource::Merged,
|
||||||
|
..TlsBehaviorProfile::default()
|
||||||
},
|
},
|
||||||
fetched_at: SystemTime::now(),
|
fetched_at: SystemTime::now(),
|
||||||
domain: "primary.example".to_string(),
|
domain: "primary.example".to_string(),
|
||||||
@@ -3933,6 +3973,22 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
output.contains("telemt_tls_front_profile_info{domain=\"fallback.example\",source=\"default\",is_default=\"true\",has_cert_info=\"false\",has_cert_payload=\"false\"} 1")
|
output.contains("telemt_tls_front_profile_info{domain=\"fallback.example\",source=\"default\",is_default=\"true\",has_cert_info=\"false\",has_cert_payload=\"false\"} 1")
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains("telemt_tls_front_profile_quality_info{domain=\"primary.example\",quality=\"raw_strict\",key_share_group=\"x25519\"} 1")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains("telemt_tls_front_profile_quality_info{domain=\"fallback.example\",quality=\"fallback\",key_share_group=\"none\"} 1")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains(
|
||||||
|
"telemt_tls_front_profile_server_hello_bytes{domain=\"primary.example\"} 52"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains(
|
||||||
|
"telemt_tls_front_profile_server_hello_extensions{domain=\"primary.example\"} 1"
|
||||||
|
)
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
output.contains(
|
output.contains(
|
||||||
"telemt_tls_front_profile_app_data_records{domain=\"primary.example\"} 2"
|
"telemt_tls_front_profile_app_data_records{domain=\"primary.example\"} 2"
|
||||||
@@ -4045,7 +4101,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(output.contains("# TYPE telemt_tls_front_profile_domains gauge"));
|
assert!(output.contains("# TYPE telemt_tls_front_profile_domains gauge"));
|
||||||
assert!(output.contains("# TYPE telemt_tls_front_profile_info gauge"));
|
assert!(output.contains("# TYPE telemt_tls_front_profile_info gauge"));
|
||||||
|
assert!(output.contains("# TYPE telemt_tls_front_profile_quality_info gauge"));
|
||||||
assert!(output.contains("# TYPE telemt_tls_front_profile_age_seconds gauge"));
|
assert!(output.contains("# TYPE telemt_tls_front_profile_age_seconds gauge"));
|
||||||
|
assert!(output.contains("# TYPE telemt_tls_front_profile_server_hello_bytes gauge"));
|
||||||
|
assert!(output.contains("# TYPE telemt_tls_front_profile_server_hello_extensions gauge"));
|
||||||
assert!(output.contains("# TYPE telemt_tls_front_profile_app_data_records gauge"));
|
assert!(output.contains("# TYPE telemt_tls_front_profile_app_data_records gauge"));
|
||||||
assert!(output.contains("# TYPE telemt_tls_front_profile_ticket_records gauge"));
|
assert!(output.contains("# TYPE telemt_tls_front_profile_ticket_records gauge"));
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -1447,6 +1447,7 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
|
|||||||
app_data_record_sizes: vec![1024],
|
app_data_record_sizes: vec![1024],
|
||||||
ticket_record_sizes: Vec::new(),
|
ticket_record_sizes: Vec::new(),
|
||||||
source: TlsProfileSource::Default,
|
source: TlsProfileSource::Default,
|
||||||
|
..TlsBehaviorProfile::default()
|
||||||
},
|
},
|
||||||
fetched_at: SystemTime::now(),
|
fetched_at: SystemTime::now(),
|
||||||
domain: "example.com".to_string(),
|
domain: "example.com".to_string(),
|
||||||
|
|||||||
+55
-9
@@ -12,7 +12,8 @@ use tokio::time::sleep;
|
|||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use crate::tls_front::types::{
|
use crate::tls_front::types::{
|
||||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsFetchResult, TlsProfileSource,
|
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsFetchResult, TlsProfileQuality,
|
||||||
|
TlsProfileSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FULL_CERT_SENT_SWEEP_INTERVAL_SECS: u64 = 30;
|
const FULL_CERT_SENT_SWEEP_INTERVAL_SECS: u64 = 30;
|
||||||
@@ -47,10 +48,14 @@ pub struct TlsFrontCache {
|
|||||||
pub(crate) struct TlsFrontProfileHealth {
|
pub(crate) struct TlsFrontProfileHealth {
|
||||||
pub(crate) domain: String,
|
pub(crate) domain: String,
|
||||||
pub(crate) source: &'static str,
|
pub(crate) source: &'static str,
|
||||||
|
pub(crate) quality: &'static str,
|
||||||
|
pub(crate) key_share_group: &'static str,
|
||||||
pub(crate) age_seconds: u64,
|
pub(crate) age_seconds: u64,
|
||||||
pub(crate) is_default: bool,
|
pub(crate) is_default: bool,
|
||||||
pub(crate) has_cert_info: bool,
|
pub(crate) has_cert_info: bool,
|
||||||
pub(crate) has_cert_payload: bool,
|
pub(crate) has_cert_payload: bool,
|
||||||
|
pub(crate) server_hello_record_len: usize,
|
||||||
|
pub(crate) server_hello_extensions: usize,
|
||||||
pub(crate) app_data_records: usize,
|
pub(crate) app_data_records: usize,
|
||||||
pub(crate) ticket_records: usize,
|
pub(crate) ticket_records: usize,
|
||||||
pub(crate) change_cipher_spec_count: u8,
|
pub(crate) change_cipher_spec_count: u8,
|
||||||
@@ -66,6 +71,23 @@ fn profile_source_label(source: TlsProfileSource) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn profile_quality_label(quality: TlsProfileQuality) -> &'static str {
|
||||||
|
match quality {
|
||||||
|
TlsProfileQuality::Fallback => "fallback",
|
||||||
|
TlsProfileQuality::RawPartial => "raw_partial",
|
||||||
|
TlsProfileQuality::RawStrict => "raw_strict",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_share_group_label(group: Option<u16>) -> &'static str {
|
||||||
|
match group {
|
||||||
|
Some(0x001d) => "x25519",
|
||||||
|
Some(0x11ec) => "x25519mlkem768",
|
||||||
|
Some(_) => "other",
|
||||||
|
None => "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl TlsFrontCache {
|
impl TlsFrontCache {
|
||||||
pub fn new(domains: &[String], default_len: usize, disk_path: impl AsRef<Path>) -> Self {
|
pub fn new(domains: &[String], default_len: usize, disk_path: impl AsRef<Path>) -> Self {
|
||||||
@@ -137,7 +159,8 @@ impl TlsFrontCache {
|
|||||||
.get(domain)
|
.get(domain)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| self.default.clone());
|
.unwrap_or_else(|| self.default.clone());
|
||||||
let behavior = &cached.behavior_profile;
|
let mut behavior = cached.behavior_profile.clone();
|
||||||
|
behavior.refresh_server_hello_summary(&cached.server_hello_template);
|
||||||
let age_seconds = now
|
let age_seconds = now
|
||||||
.duration_since(cached.fetched_at)
|
.duration_since(cached.fetched_at)
|
||||||
.map(|duration| duration.as_secs())
|
.map(|duration| duration.as_secs())
|
||||||
@@ -146,10 +169,14 @@ impl TlsFrontCache {
|
|||||||
snapshot.push(TlsFrontProfileHealth {
|
snapshot.push(TlsFrontProfileHealth {
|
||||||
domain: domain.clone(),
|
domain: domain.clone(),
|
||||||
source: profile_source_label(behavior.source),
|
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,
|
age_seconds,
|
||||||
is_default: cached.domain == "default",
|
is_default: cached.domain == "default",
|
||||||
has_cert_info: cached.cert_info.is_some(),
|
has_cert_info: cached.cert_info.is_some(),
|
||||||
has_cert_payload: cached.cert_payload.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: cached
|
||||||
.app_data_records_sizes
|
.app_data_records_sizes
|
||||||
.len()
|
.len()
|
||||||
@@ -378,20 +405,39 @@ impl TlsFrontCache {
|
|||||||
|
|
||||||
/// Replace cached entry from a fetch result.
|
/// Replace cached entry from a fetch result.
|
||||||
pub async fn update_from_fetch(&self, domain: &str, fetched: TlsFetchResult) {
|
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 {
|
let data = CachedTlsData {
|
||||||
server_hello_template: fetched.server_hello_parsed,
|
server_hello_template: server_hello_parsed,
|
||||||
cert_info: fetched.cert_info,
|
cert_info,
|
||||||
cert_payload: fetched.cert_payload,
|
cert_payload,
|
||||||
app_data_records_sizes: fetched.app_data_records_sizes.clone(),
|
app_data_records_sizes: app_data_records_sizes.clone(),
|
||||||
total_app_data_len: fetched.total_app_data_len,
|
total_app_data_len,
|
||||||
behavior_profile: fetched.behavior_profile,
|
behavior_profile,
|
||||||
fetched_at: SystemTime::now(),
|
fetched_at: SystemTime::now(),
|
||||||
domain: domain.to_string(),
|
domain: domain.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.set(domain, data.clone()).await;
|
self.set(domain, data.clone()).await;
|
||||||
self.persist(domain, &data).await;
|
self.persist(domain, &data).await;
|
||||||
debug!(domain = %domain, len = fetched.total_app_data_len, "TLS cache updated");
|
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> {
|
pub fn default_entry(&self) -> Arc<CachedTlsData> {
|
||||||
|
|||||||
@@ -838,6 +838,7 @@ fn derive_behavior_profile(records: &[(u8, Vec<u8>)]) -> TlsBehaviorProfile {
|
|||||||
app_data_record_sizes,
|
app_data_record_sizes,
|
||||||
ticket_record_sizes,
|
ticket_record_sizes,
|
||||||
source: TlsProfileSource::Raw,
|
source: TlsProfileSource::Raw,
|
||||||
|
..TlsBehaviorProfile::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,14 +1088,18 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut server_hello = None;
|
let mut server_hello = None;
|
||||||
|
let mut server_hello_record_len = 0usize;
|
||||||
for (t, body) in &records {
|
for (t, body) in &records {
|
||||||
if *t == TLS_RECORD_HANDSHAKE && server_hello.is_none() {
|
if *t == TLS_RECORD_HANDSHAKE && server_hello.is_none() {
|
||||||
server_hello = parse_server_hello(body);
|
server_hello = parse_server_hello(body);
|
||||||
|
server_hello_record_len = body.len();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed = server_hello.ok_or_else(|| anyhow!("ServerHello not received"))?;
|
let parsed = server_hello.ok_or_else(|| anyhow!("ServerHello not received"))?;
|
||||||
let behavior_profile = derive_behavior_profile(&records);
|
let mut behavior_profile = derive_behavior_profile(&records);
|
||||||
|
behavior_profile.server_hello_record_len = server_hello_record_len;
|
||||||
|
behavior_profile.refresh_server_hello_summary(&parsed);
|
||||||
let mut app_sizes = behavior_profile.app_data_record_sizes.clone();
|
let mut app_sizes = behavior_profile.app_data_record_sizes.clone();
|
||||||
app_sizes.extend_from_slice(&behavior_profile.ticket_record_sizes);
|
app_sizes.extend_from_slice(&behavior_profile.ticket_record_sizes);
|
||||||
let total_app_data_len = app_sizes.iter().sum::<usize>().max(1024);
|
let total_app_data_len = app_sizes.iter().sum::<usize>().max(1024);
|
||||||
@@ -1272,6 +1277,7 @@ where
|
|||||||
app_data_record_sizes: app_data_records_sizes,
|
app_data_record_sizes: app_data_records_sizes,
|
||||||
ticket_record_sizes: Vec::new(),
|
ticket_record_sizes: Vec::new(),
|
||||||
source: TlsProfileSource::Rustls,
|
source: TlsProfileSource::Rustls,
|
||||||
|
..TlsBehaviorProfile::default()
|
||||||
},
|
},
|
||||||
cert_info,
|
cert_info,
|
||||||
cert_payload,
|
cert_payload,
|
||||||
@@ -1471,6 +1477,7 @@ pub async fn fetch_real_tls_with_strategy(
|
|||||||
raw.cert_info = rustls.cert_info;
|
raw.cert_info = rustls.cert_info;
|
||||||
raw.cert_payload = rustls.cert_payload;
|
raw.cert_payload = rustls.cert_payload;
|
||||||
raw.behavior_profile.source = TlsProfileSource::Merged;
|
raw.behavior_profile.source = TlsProfileSource::Merged;
|
||||||
|
raw.behavior_profile.refresh_quality();
|
||||||
debug!(sni = %sni, "Fetched TLS metadata via adaptive raw probe + rustls cert chain");
|
debug!(sni = %sni, "Fetched TLS metadata via adaptive raw probe + rustls cert chain");
|
||||||
Ok(raw)
|
Ok(raw)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fn make_cached() -> CachedTlsData {
|
|||||||
app_data_record_sizes: vec![1200, 900],
|
app_data_record_sizes: vec![1200, 900],
|
||||||
ticket_record_sizes: vec![220, 180],
|
ticket_record_sizes: vec![220, 180],
|
||||||
source: TlsProfileSource::Merged,
|
source: TlsProfileSource::Merged,
|
||||||
|
..TlsBehaviorProfile::default()
|
||||||
},
|
},
|
||||||
fetched_at: SystemTime::now(),
|
fetched_at: SystemTime::now(),
|
||||||
domain: "example.com".to_string(),
|
domain: "example.com".to_string(),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fn make_cached(cert_payload: Option<crate::tls_front::types::TlsCertPayload>) ->
|
|||||||
app_data_record_sizes: vec![64],
|
app_data_record_sizes: vec![64],
|
||||||
ticket_record_sizes: Vec::new(),
|
ticket_record_sizes: Vec::new(),
|
||||||
source: TlsProfileSource::Default,
|
source: TlsProfileSource::Default,
|
||||||
|
..TlsBehaviorProfile::default()
|
||||||
},
|
},
|
||||||
fetched_at: SystemTime::now(),
|
fetched_at: SystemTime::now(),
|
||||||
domain: "example.com".to_string(),
|
domain: "example.com".to_string(),
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
const EXT_KEY_SHARE: u16 = 0x0033;
|
||||||
|
const TLS_NAMED_GROUP_X25519: u16 = 0x001d;
|
||||||
|
const TLS_NAMED_GROUP_X25519MLKEM768: u16 = 0x11ec;
|
||||||
|
|
||||||
/// Parsed representation of an unencrypted TLS ServerHello.
|
/// Parsed representation of an unencrypted TLS ServerHello.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ParsedServerHello {
|
pub struct ParsedServerHello {
|
||||||
@@ -19,6 +23,52 @@ pub struct TlsExtension {
|
|||||||
pub data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ParsedServerHello {
|
||||||
|
/// Return the TLS record body length that would contain this ServerHello.
|
||||||
|
pub(crate) fn record_body_len(&self) -> usize {
|
||||||
|
let extensions_len = self
|
||||||
|
.extensions
|
||||||
|
.iter()
|
||||||
|
.map(|extension| 4 + extension.data.len())
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
4 + 2 + 32 + 1 + self.session_id.len() + 2 + 1 + 2 + extensions_len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return visible ServerHello extension types in wire order.
|
||||||
|
pub(crate) fn extension_types(&self) -> Vec<u16> {
|
||||||
|
self.extensions
|
||||||
|
.iter()
|
||||||
|
.map(|extension| extension.ext_type)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a replay-safe ServerHello key_share group when the extension is well-formed.
|
||||||
|
pub(crate) fn key_share_group(&self) -> Option<u16> {
|
||||||
|
self.extensions
|
||||||
|
.iter()
|
||||||
|
.find(|extension| extension.ext_type == EXT_KEY_SHARE)
|
||||||
|
.and_then(|extension| parse_key_share_group(&extension.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_key_share_group(data: &[u8]) -> Option<u16> {
|
||||||
|
if data.len() < 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = u16::from_be_bytes([data[0], data[1]]);
|
||||||
|
let key_exchange_len = u16::from_be_bytes([data[2], data[3]]) as usize;
|
||||||
|
if data.len() != 4 + key_exchange_len {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match group {
|
||||||
|
TLS_NAMED_GROUP_X25519 | TLS_NAMED_GROUP_X25519MLKEM768 => Some(group),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Basic certificate metadata (optional, informative).
|
/// Basic certificate metadata (optional, informative).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ParsedCertificateInfo {
|
pub struct ParsedCertificateInfo {
|
||||||
@@ -54,6 +104,19 @@ pub enum TlsProfileSource {
|
|||||||
Merged,
|
Merged,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// DPI-facing quality class of a cached TLS front profile.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum TlsProfileQuality {
|
||||||
|
/// No raw origin ServerHello shape is available.
|
||||||
|
#[default]
|
||||||
|
Fallback,
|
||||||
|
/// Raw origin ServerHello was captured, but encrypted flight shape is incomplete.
|
||||||
|
RawPartial,
|
||||||
|
/// Raw origin ServerHello and encrypted flight record sizes were captured.
|
||||||
|
RawStrict,
|
||||||
|
}
|
||||||
|
|
||||||
/// Coarse-grained TLS response behavior captured per SNI.
|
/// Coarse-grained TLS response behavior captured per SNI.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TlsBehaviorProfile {
|
pub struct TlsBehaviorProfile {
|
||||||
@@ -69,6 +132,18 @@ pub struct TlsBehaviorProfile {
|
|||||||
/// Source of this behavior profile.
|
/// Source of this behavior profile.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub source: TlsProfileSource,
|
pub source: TlsProfileSource,
|
||||||
|
/// DPI-facing quality of this profile.
|
||||||
|
#[serde(default)]
|
||||||
|
pub quality: TlsProfileQuality,
|
||||||
|
/// Captured ServerHello TLS record body length.
|
||||||
|
#[serde(default)]
|
||||||
|
pub server_hello_record_len: usize,
|
||||||
|
/// Captured visible ServerHello extension types in wire order.
|
||||||
|
#[serde(default)]
|
||||||
|
pub server_hello_extension_types: Vec<u16>,
|
||||||
|
/// Captured ServerHello key_share group when replay-safe.
|
||||||
|
#[serde(default)]
|
||||||
|
pub server_hello_key_share_group: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_change_cipher_spec_count() -> u8 {
|
fn default_change_cipher_spec_count() -> u8 {
|
||||||
@@ -82,10 +157,46 @@ impl Default for TlsBehaviorProfile {
|
|||||||
app_data_record_sizes: Vec::new(),
|
app_data_record_sizes: Vec::new(),
|
||||||
ticket_record_sizes: Vec::new(),
|
ticket_record_sizes: Vec::new(),
|
||||||
source: TlsProfileSource::Default,
|
source: TlsProfileSource::Default,
|
||||||
|
quality: TlsProfileQuality::Fallback,
|
||||||
|
server_hello_record_len: 0,
|
||||||
|
server_hello_extension_types: Vec::new(),
|
||||||
|
server_hello_key_share_group: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TlsBehaviorProfile {
|
||||||
|
/// Refresh cached visible ServerHello summary fields and quality.
|
||||||
|
pub(crate) fn refresh_server_hello_summary(&mut self, server_hello: &ParsedServerHello) {
|
||||||
|
if matches!(self.source, TlsProfileSource::Raw | TlsProfileSource::Merged) {
|
||||||
|
if self.server_hello_record_len == 0 {
|
||||||
|
self.server_hello_record_len = server_hello.record_body_len();
|
||||||
|
}
|
||||||
|
self.server_hello_extension_types = server_hello.extension_types();
|
||||||
|
self.server_hello_key_share_group = server_hello.key_share_group();
|
||||||
|
} else {
|
||||||
|
self.server_hello_record_len = 0;
|
||||||
|
self.server_hello_extension_types.clear();
|
||||||
|
self.server_hello_key_share_group = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refresh_quality();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompute the profile quality from current source and record-size evidence.
|
||||||
|
pub(crate) fn refresh_quality(&mut self) {
|
||||||
|
let has_raw_server_hello = matches!(self.source, TlsProfileSource::Raw | TlsProfileSource::Merged)
|
||||||
|
&& self.server_hello_record_len > 0;
|
||||||
|
self.quality = if has_raw_server_hello && !self.app_data_record_sizes.is_empty() {
|
||||||
|
TlsProfileQuality::RawStrict
|
||||||
|
} else if has_raw_server_hello {
|
||||||
|
TlsProfileQuality::RawPartial
|
||||||
|
} else {
|
||||||
|
TlsProfileQuality::Fallback
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Cached data per SNI used by the emulator.
|
/// Cached data per SNI used by the emulator.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CachedTlsData {
|
pub struct CachedTlsData {
|
||||||
@@ -147,5 +258,6 @@ mod tests {
|
|||||||
assert!(cached.behavior_profile.app_data_record_sizes.is_empty());
|
assert!(cached.behavior_profile.app_data_record_sizes.is_empty());
|
||||||
assert!(cached.behavior_profile.ticket_record_sizes.is_empty());
|
assert!(cached.behavior_profile.ticket_record_sizes.is_empty());
|
||||||
assert_eq!(cached.behavior_profile.source, TlsProfileSource::Default);
|
assert_eq!(cached.behavior_profile.source, TlsProfileSource::Default);
|
||||||
|
assert_eq!(cached.behavior_profile.quality, TlsProfileQuality::Fallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user