#[derive(Debug, Clone)]
pub struct TlsFingerprint {
pub name: String,
pub ja3: String,
pub alpn: Vec<String>,
pub cipher_suites: Vec<String>,
pub extensions: Vec<String>,
}
impl TlsFingerprint {
pub fn as_header_hints(&self) -> Vec<(String, String)> {
vec![
("Sec-CH-UA-Platform".to_string(), "".to_string()),
("X-TLS-Fingerprint".to_string(), self.ja3.clone()),
]
}
}
#[derive(Debug, Clone)]
pub struct TlsRotationPolicy {
pub profiles: Vec<TlsFingerprint>,
pub enable: bool,
pub rotate: bool,
}
impl Default for TlsRotationPolicy {
fn default() -> Self {
Self::with_defaults()
}
}
impl TlsRotationPolicy {
pub fn from_config(cfg: TlsRotationConfig) -> Self {
let mut policy = Self::with_defaults();
policy.enable = cfg.enabled;
policy.rotate = cfg.enabled;
policy
}
pub fn with_defaults() -> Self {
Self {
profiles: Self::build_default_profiles(),
enable: true,
rotate: true,
}
}
pub fn build_default_profiles() -> Vec<TlsFingerprint> {
vec![
TlsFingerprint {
name: "chrome-desktop-121".to_string(),
ja3: "770,4865-4866-4867,23-24,0-11-10-35-16-22".to_string(),
alpn: vec!["h2".into(), "http/1.1".into()],
cipher_suites: vec!["TLS_AES_128_GCM_SHA256".into(), "TLS_AES_256_GCM_SHA384".into()],
extensions: vec!["server_name".into(), "application_layer_protocol_negotiation".into()],
},
TlsFingerprint {
name: "chrome-mobile-121".to_string(),
ja3: "771,4865-4867,23-24-25,0-11-10-35-16".to_string(),
alpn: vec!["h2".into(), "http/1.1".into()],
cipher_suites: vec!["TLS_CHACHA20_POLY1305_SHA256".into(), "TLS_AES_128_GCM_SHA256".into()],
extensions: vec!["server_name".into(), "extended_master_secret".into()],
},
TlsFingerprint {
name: "firefox-desktop".to_string(),
ja3: "772,4867-4866-4865,23-24,0-10-11-16-35".to_string(),
alpn: vec!["h2".into(), "http/1.1".into()],
cipher_suites: vec!["TLS_AES_128_GCM_SHA256".into(), "TLS_CHACHA20_POLY1305_SHA256".into()],
extensions: vec!["server_name".into(), "key_share".into()],
},
]
}
pub fn rotate(&self, rng: &mut impl rand::Rng) -> TlsFingerprint {
if self.profiles.is_empty() {
return TlsFingerprint {
name: "fallback".to_string(),
ja3: "unknown".to_string(),
alpn: vec!["h2".into()],
cipher_suites: vec!["TLS_AES_128_GCM_SHA256".into()],
extensions: vec!["server_name".into()],
};
}
self.profiles[rng.gen_range(0..self.profiles.len())].clone()
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TlsRotationConfig {
pub enabled: bool,
}
impl Default for TlsRotationConfig {
fn default() -> Self {
Self { enabled: true }
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{SeedableRng, rngs::StdRng};
#[test]
fn default_profiles_are_stable() {
let policy = TlsRotationPolicy::default();
let mut rng = StdRng::seed_from_u64(99);
let a = policy.rotate(&mut rng);
let mut rng = StdRng::seed_from_u64(99);
let b = policy.rotate(&mut rng);
assert_eq!(a.name, b.name);
}
#[test]
fn fallback_has_headers() {
let profile = TlsRotationPolicy { profiles: vec![], enable: true, rotate: false };
let fp = profile.rotate(&mut StdRng::seed_from_u64(1));
assert_eq!(fp.name, "fallback");
assert_eq!(fp.as_header_hints()[1].0, "X-TLS-Fingerprint");
}
}