mirror of https://github.com/telemt/telemt.git
TLS-F New Methods
This commit is contained in:
parent
4ddbb97908
commit
4677b43c6e
|
|
@ -8,7 +8,9 @@ use tokio::sync::RwLock;
|
|||
use tokio::time::sleep;
|
||||
use tracing::{debug, warn, info};
|
||||
|
||||
use crate::tls_front::types::{CachedTlsData, ParsedServerHello, TlsFetchResult};
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsFetchResult,
|
||||
};
|
||||
|
||||
/// Lightweight in-memory + optional on-disk cache for TLS fronting data.
|
||||
#[derive(Debug)]
|
||||
|
|
@ -37,6 +39,7 @@ impl TlsFrontCache {
|
|||
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(),
|
||||
});
|
||||
|
|
@ -189,6 +192,7 @@ impl TlsFrontCache {
|
|||
cert_payload: fetched.cert_payload,
|
||||
app_data_records_sizes: fetched.app_data_records_sizes.clone(),
|
||||
total_app_data_len: fetched.total_app_data_len,
|
||||
behavior_profile: fetched.behavior_profile,
|
||||
fetched_at: SystemTime::now(),
|
||||
domain: domain.to_string(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crate::protocol::constants::{
|
|||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE, TLS_VERSION,
|
||||
};
|
||||
use crate::protocol::tls::{TLS_DIGEST_LEN, TLS_DIGEST_POS, gen_fake_x25519_key};
|
||||
use crate::tls_front::types::{CachedTlsData, ParsedCertificateInfo};
|
||||
use crate::tls_front::types::{CachedTlsData, ParsedCertificateInfo, TlsProfileSource};
|
||||
|
||||
const MIN_APP_DATA: usize = 64;
|
||||
const MAX_APP_DATA: usize = 16640; // RFC 8446 §5.2 allows up to 2^14 + 256
|
||||
|
|
@ -108,14 +108,12 @@ pub fn build_emulated_server_hello(
|
|||
) -> Vec<u8> {
|
||||
// --- ServerHello ---
|
||||
let mut extensions = Vec::new();
|
||||
// KeyShare (x25519)
|
||||
let key = gen_fake_x25519_key(rng);
|
||||
extensions.extend_from_slice(&0x0033u16.to_be_bytes()); // key_share
|
||||
extensions.extend_from_slice(&(2 + 2 + 32u16).to_be_bytes()); // len
|
||||
extensions.extend_from_slice(&0x001du16.to_be_bytes()); // X25519
|
||||
extensions.extend_from_slice(&0x0033u16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(2 + 2 + 32u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&0x001du16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(32u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&key);
|
||||
// supported_versions (TLS1.3)
|
||||
extensions.extend_from_slice(&0x002bu16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(2u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&0x0304u16.to_be_bytes());
|
||||
|
|
@ -128,7 +126,6 @@ pub fn build_emulated_server_hello(
|
|||
extensions.push(alpn_proto.len() as u8);
|
||||
extensions.extend_from_slice(alpn_proto);
|
||||
}
|
||||
|
||||
let extensions_len = extensions.len() as u16;
|
||||
|
||||
let body_len = 2 + // version
|
||||
|
|
@ -173,11 +170,22 @@ pub fn build_emulated_server_hello(
|
|||
];
|
||||
|
||||
// --- ApplicationData (fake encrypted records) ---
|
||||
// Use the same number and sizes of ApplicationData records as the cached server.
|
||||
let mut sizes = cached.app_data_records_sizes.clone();
|
||||
if sizes.is_empty() {
|
||||
sizes.push(cached.total_app_data_len.max(1024));
|
||||
}
|
||||
let sizes = match cached.behavior_profile.source {
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged => cached
|
||||
.app_data_records_sizes
|
||||
.first()
|
||||
.copied()
|
||||
.or_else(|| cached.behavior_profile.app_data_record_sizes.first().copied())
|
||||
.map(|size| vec![size])
|
||||
.unwrap_or_else(|| vec![cached.total_app_data_len.max(1024)]),
|
||||
_ => {
|
||||
let mut sizes = cached.app_data_records_sizes.clone();
|
||||
if sizes.is_empty() {
|
||||
sizes.push(cached.total_app_data_len.max(1024));
|
||||
}
|
||||
sizes
|
||||
}
|
||||
};
|
||||
let mut sizes = jitter_and_clamp_sizes(&sizes, rng);
|
||||
let compact_payload = cached
|
||||
.cert_info
|
||||
|
|
@ -269,7 +277,9 @@ pub fn build_emulated_server_hello(
|
|||
mod tests {
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::tls_front::types::{CachedTlsData, ParsedServerHello, TlsCertPayload};
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsProfileSource,
|
||||
};
|
||||
|
||||
use super::build_emulated_server_hello;
|
||||
use crate::crypto::SecureRandom;
|
||||
|
|
@ -300,6 +310,7 @@ mod tests {
|
|||
cert_payload,
|
||||
app_data_records_sizes: vec![64],
|
||||
total_app_data_len: 64,
|
||||
behavior_profile: TlsBehaviorProfile::default(),
|
||||
fetched_at: SystemTime::now(),
|
||||
domain: "example.com".to_string(),
|
||||
}
|
||||
|
|
@ -385,4 +396,34 @@ mod tests {
|
|||
let payload = first_app_data_payload(&response);
|
||||
assert!(payload.starts_with(b"CN=example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_ignores_tail_records_for_raw_profile() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.app_data_records_sizes = vec![27, 3905, 537, 69];
|
||||
cached.total_app_data_len = 4538;
|
||||
cached.behavior_profile.source = TlsProfileSource::Merged;
|
||||
cached.behavior_profile.app_data_record_sizes = vec![27, 3905, 537];
|
||||
cached.behavior_profile.ticket_record_sizes = vec![69];
|
||||
|
||||
let rng = SecureRandom::new();
|
||||
let response = build_emulated_server_hello(
|
||||
b"secret",
|
||||
&[0x12; 32],
|
||||
&[0x34; 16],
|
||||
&cached,
|
||||
false,
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
let hello_len = u16::from_be_bytes([response[3], response[4]]) as usize;
|
||||
let ccs_start = 5 + hello_len;
|
||||
let app_start = ccs_start + 6;
|
||||
let app_len = u16::from_be_bytes([response[app_start + 3], response[app_start + 4]]) as usize;
|
||||
|
||||
assert_eq!(response[app_start], TLS_RECORD_APPLICATION);
|
||||
assert_eq!(app_start + 5 + app_len, response.len());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,14 +21,18 @@ use x509_parser::certificate::X509Certificate;
|
|||
|
||||
use crate::crypto::SecureRandom;
|
||||
use crate::network::dns_overrides::resolve_socket_addr;
|
||||
use crate::protocol::constants::{TLS_RECORD_APPLICATION, TLS_RECORD_HANDSHAKE};
|
||||
use crate::protocol::constants::{
|
||||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE,
|
||||
};
|
||||
use crate::transport::proxy_protocol::{ProxyProtocolV1Builder, ProxyProtocolV2Builder};
|
||||
use crate::tls_front::types::{
|
||||
ParsedCertificateInfo,
|
||||
ParsedServerHello,
|
||||
TlsBehaviorProfile,
|
||||
TlsCertPayload,
|
||||
TlsExtension,
|
||||
TlsFetchResult,
|
||||
TlsProfileSource,
|
||||
};
|
||||
|
||||
/// No-op verifier: accept any certificate (we only need lengths and metadata).
|
||||
|
|
@ -282,6 +286,41 @@ fn parse_server_hello(body: &[u8]) -> Option<ParsedServerHello> {
|
|||
})
|
||||
}
|
||||
|
||||
fn derive_behavior_profile(records: &[(u8, Vec<u8>)]) -> TlsBehaviorProfile {
|
||||
let mut change_cipher_spec_count = 0u8;
|
||||
let mut app_data_record_sizes = Vec::new();
|
||||
|
||||
for (record_type, body) in records {
|
||||
match *record_type {
|
||||
TLS_RECORD_CHANGE_CIPHER => {
|
||||
change_cipher_spec_count = change_cipher_spec_count.saturating_add(1);
|
||||
}
|
||||
TLS_RECORD_APPLICATION => {
|
||||
app_data_record_sizes.push(body.len());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut ticket_record_sizes = Vec::new();
|
||||
while app_data_record_sizes
|
||||
.last()
|
||||
.is_some_and(|size| *size <= 256 && ticket_record_sizes.len() < 2)
|
||||
{
|
||||
if let Some(size) = app_data_record_sizes.pop() {
|
||||
ticket_record_sizes.push(size);
|
||||
}
|
||||
}
|
||||
ticket_record_sizes.reverse();
|
||||
|
||||
TlsBehaviorProfile {
|
||||
change_cipher_spec_count: change_cipher_spec_count.max(1),
|
||||
app_data_record_sizes,
|
||||
ticket_record_sizes,
|
||||
source: TlsProfileSource::Raw,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cert_info(certs: &[CertificateDer<'static>]) -> Option<ParsedCertificateInfo> {
|
||||
let first = certs.first()?;
|
||||
let (_rem, cert) = X509Certificate::from_der(first.as_ref()).ok()?;
|
||||
|
|
@ -443,39 +482,50 @@ where
|
|||
.await??;
|
||||
|
||||
let mut records = Vec::new();
|
||||
// Read up to 4 records: ServerHello, CCS, and up to two ApplicationData.
|
||||
for _ in 0..4 {
|
||||
let mut app_records_seen = 0usize;
|
||||
// Read a bounded encrypted flight: ServerHello, CCS, certificate-like data,
|
||||
// and a small number of ticket-like tail records.
|
||||
for _ in 0..8 {
|
||||
match timeout(connect_timeout, read_tls_record(&mut stream)).await {
|
||||
Ok(Ok(rec)) => records.push(rec),
|
||||
Ok(Ok(rec)) => {
|
||||
if rec.0 == TLS_RECORD_APPLICATION {
|
||||
app_records_seen += 1;
|
||||
}
|
||||
records.push(rec);
|
||||
}
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(_) => break,
|
||||
}
|
||||
if records.len() >= 3 && records.iter().any(|(t, _)| *t == TLS_RECORD_APPLICATION) {
|
||||
if app_records_seen >= 4 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut app_sizes = Vec::new();
|
||||
let mut server_hello = None;
|
||||
for (t, body) in &records {
|
||||
if *t == TLS_RECORD_HANDSHAKE && server_hello.is_none() {
|
||||
server_hello = parse_server_hello(body);
|
||||
} else if *t == TLS_RECORD_APPLICATION {
|
||||
app_sizes.push(body.len());
|
||||
}
|
||||
}
|
||||
|
||||
let parsed = server_hello.ok_or_else(|| anyhow!("ServerHello not received"))?;
|
||||
let behavior_profile = derive_behavior_profile(&records);
|
||||
let mut app_sizes = behavior_profile.app_data_record_sizes.clone();
|
||||
app_sizes.extend_from_slice(&behavior_profile.ticket_record_sizes);
|
||||
let total_app_data_len = app_sizes.iter().sum::<usize>().max(1024);
|
||||
let app_data_records_sizes = behavior_profile
|
||||
.app_data_record_sizes
|
||||
.first()
|
||||
.copied()
|
||||
.or_else(|| behavior_profile.ticket_record_sizes.first().copied())
|
||||
.map(|size| vec![size])
|
||||
.unwrap_or_else(|| vec![total_app_data_len]);
|
||||
|
||||
Ok(TlsFetchResult {
|
||||
server_hello_parsed: parsed,
|
||||
app_data_records_sizes: if app_sizes.is_empty() {
|
||||
vec![total_app_data_len]
|
||||
} else {
|
||||
app_sizes
|
||||
},
|
||||
app_data_records_sizes,
|
||||
total_app_data_len,
|
||||
behavior_profile,
|
||||
cert_info: None,
|
||||
cert_payload: None,
|
||||
})
|
||||
|
|
@ -608,6 +658,12 @@ where
|
|||
server_hello_parsed: parsed,
|
||||
app_data_records_sizes: app_data_records_sizes.clone(),
|
||||
total_app_data_len: app_data_records_sizes.iter().sum(),
|
||||
behavior_profile: TlsBehaviorProfile {
|
||||
change_cipher_spec_count: 1,
|
||||
app_data_record_sizes: app_data_records_sizes,
|
||||
ticket_record_sizes: Vec::new(),
|
||||
source: TlsProfileSource::Rustls,
|
||||
},
|
||||
cert_info,
|
||||
cert_payload,
|
||||
})
|
||||
|
|
@ -706,6 +762,7 @@ pub async fn fetch_real_tls(
|
|||
if let Some(mut raw) = raw_result {
|
||||
raw.cert_info = rustls_result.cert_info;
|
||||
raw.cert_payload = rustls_result.cert_payload;
|
||||
raw.behavior_profile.source = TlsProfileSource::Merged;
|
||||
debug!(sni = %sni, "Fetched TLS metadata via raw probe + rustls cert chain");
|
||||
Ok(raw)
|
||||
} else {
|
||||
|
|
@ -725,7 +782,11 @@ pub async fn fetch_real_tls(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::encode_tls13_certificate_message;
|
||||
use super::{derive_behavior_profile, encode_tls13_certificate_message};
|
||||
use crate::protocol::constants::{
|
||||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE,
|
||||
};
|
||||
use crate::tls_front::types::TlsProfileSource;
|
||||
|
||||
fn read_u24(bytes: &[u8]) -> usize {
|
||||
((bytes[0] as usize) << 16) | ((bytes[1] as usize) << 8) | (bytes[2] as usize)
|
||||
|
|
@ -753,4 +814,20 @@ mod tests {
|
|||
fn test_encode_tls13_certificate_message_empty_chain() {
|
||||
assert!(encode_tls13_certificate_message(&[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_behavior_profile_splits_ticket_like_tail_records() {
|
||||
let profile = derive_behavior_profile(&[
|
||||
(TLS_RECORD_HANDSHAKE, vec![0u8; 90]),
|
||||
(TLS_RECORD_CHANGE_CIPHER, vec![0x01]),
|
||||
(TLS_RECORD_APPLICATION, vec![0u8; 1400]),
|
||||
(TLS_RECORD_APPLICATION, vec![0u8; 220]),
|
||||
(TLS_RECORD_APPLICATION, vec![0u8; 180]),
|
||||
]);
|
||||
|
||||
assert_eq!(profile.change_cipher_spec_count, 1);
|
||||
assert_eq!(profile.app_data_record_sizes, vec![1400]);
|
||||
assert_eq!(profile.ticket_record_sizes, vec![220, 180]);
|
||||
assert_eq!(profile.source, TlsProfileSource::Raw);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,53 @@ pub struct TlsCertPayload {
|
|||
pub certificate_message: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Provenance of the cached TLS behavior profile.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TlsProfileSource {
|
||||
/// Built from hardcoded defaults or legacy cache entries.
|
||||
#[default]
|
||||
Default,
|
||||
/// Derived from raw TLS record capture only.
|
||||
Raw,
|
||||
/// Derived from rustls-only metadata fallback.
|
||||
Rustls,
|
||||
/// Merged from raw TLS capture and rustls certificate metadata.
|
||||
Merged,
|
||||
}
|
||||
|
||||
/// Coarse-grained TLS response behavior captured per SNI.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TlsBehaviorProfile {
|
||||
/// Number of ChangeCipherSpec records observed before encrypted flight.
|
||||
#[serde(default = "default_change_cipher_spec_count")]
|
||||
pub change_cipher_spec_count: u8,
|
||||
/// Sizes of the primary encrypted flight records carrying cert-like payload.
|
||||
#[serde(default)]
|
||||
pub app_data_record_sizes: Vec<usize>,
|
||||
/// Sizes of small tail ApplicationData records that look like tickets.
|
||||
#[serde(default)]
|
||||
pub ticket_record_sizes: Vec<usize>,
|
||||
/// Source of this behavior profile.
|
||||
#[serde(default)]
|
||||
pub source: TlsProfileSource,
|
||||
}
|
||||
|
||||
fn default_change_cipher_spec_count() -> u8 {
|
||||
1
|
||||
}
|
||||
|
||||
impl Default for TlsBehaviorProfile {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
change_cipher_spec_count: default_change_cipher_spec_count(),
|
||||
app_data_record_sizes: Vec::new(),
|
||||
ticket_record_sizes: Vec::new(),
|
||||
source: TlsProfileSource::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached data per SNI used by the emulator.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CachedTlsData {
|
||||
|
|
@ -48,6 +95,8 @@ pub struct CachedTlsData {
|
|||
pub cert_payload: Option<TlsCertPayload>,
|
||||
pub app_data_records_sizes: Vec<usize>,
|
||||
pub total_app_data_len: usize,
|
||||
#[serde(default)]
|
||||
pub behavior_profile: TlsBehaviorProfile,
|
||||
#[serde(default = "now_system_time", skip_serializing, skip_deserializing)]
|
||||
pub fetched_at: SystemTime,
|
||||
pub domain: String,
|
||||
|
|
@ -63,6 +112,40 @@ pub struct TlsFetchResult {
|
|||
pub server_hello_parsed: ParsedServerHello,
|
||||
pub app_data_records_sizes: Vec<usize>,
|
||||
pub total_app_data_len: usize,
|
||||
#[serde(default)]
|
||||
pub behavior_profile: TlsBehaviorProfile,
|
||||
pub cert_info: Option<ParsedCertificateInfo>,
|
||||
pub cert_payload: Option<TlsCertPayload>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cached_tls_data_deserializes_without_behavior_profile() {
|
||||
let json = r#"
|
||||
{
|
||||
"server_hello_template": {
|
||||
"version": [3, 3],
|
||||
"random": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"session_id": [],
|
||||
"cipher_suite": [19, 1],
|
||||
"compression": 0,
|
||||
"extensions": []
|
||||
},
|
||||
"cert_info": null,
|
||||
"cert_payload": null,
|
||||
"app_data_records_sizes": [1024],
|
||||
"total_app_data_len": 1024,
|
||||
"domain": "example.com"
|
||||
}
|
||||
"#;
|
||||
|
||||
let cached: CachedTlsData = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(cached.behavior_profile.change_cipher_spec_count, 1);
|
||||
assert!(cached.behavior_profile.app_data_record_sizes.is_empty());
|
||||
assert!(cached.behavior_profile.ticket_record_sizes.is_empty());
|
||||
assert_eq!(cached.behavior_profile.source, TlsProfileSource::Default);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue