use crate::Result;
use crate::data::client_data::{CLIENT_DB, ClientProfile};
use crate::protocols::Protocol;
use crate::utils::network::Target;
use rustls::{ClientConfig, RootCertStore};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::timeout;
#[derive(Debug, Clone)]
struct HandshakeInfo {
protocol: Protocol,
cipher: Option<String>,
alpn: Option<String>,
key_exchange: Option<String>,
certificate_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientSimulationResult {
pub client_name: String,
pub client_id: String,
pub success: bool,
pub protocol: Option<Protocol>,
pub cipher: Option<String>,
pub error: Option<String>,
pub handshake_time_ms: Option<u64>,
pub alpn: Option<String>,
pub key_exchange: Option<String>,
pub forward_secrecy: bool,
pub certificate_type: Option<String>,
}
pub struct ClientSimulator {
target: Target,
connect_timeout: Duration,
read_timeout: Duration,
}
impl ClientSimulator {
pub fn new(target: Target) -> Self {
Self {
target,
connect_timeout: Duration::from_secs(10),
read_timeout: Duration::from_secs(5),
}
}
pub async fn simulate_all_clients(&self) -> Result<Vec<ClientSimulationResult>> {
let clients = CLIENT_DB.current_clients();
let mut results = Vec::new();
for client in clients {
let result = self.simulate_client(client).await;
results.push(result);
}
Ok(results)
}
pub async fn simulate_client_by_id(&self, client_id: &str) -> Result<ClientSimulationResult> {
let client = CLIENT_DB
.get_by_id(client_id)
.ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?;
Ok(self.simulate_client(client).await)
}
async fn simulate_client(&self, client: &ClientProfile) -> ClientSimulationResult {
let start = std::time::Instant::now();
match self.try_connect_as_client(client).await {
Ok(handshake_info) => {
let has_fs = handshake_info
.cipher
.as_ref()
.map(|c| {
c.contains("ECDHE")
|| c.contains("DHE")
|| c.starts_with("TLS_AES")
|| c.starts_with("TLS_CHACHA20")
})
.unwrap_or(false);
ClientSimulationResult {
client_name: client.name.clone(),
client_id: client.short_id.clone(),
success: true,
protocol: Some(handshake_info.protocol),
cipher: handshake_info.cipher,
error: None,
handshake_time_ms: Some(start.elapsed().as_millis() as u64),
alpn: handshake_info.alpn,
key_exchange: handshake_info.key_exchange,
forward_secrecy: has_fs,
certificate_type: handshake_info.certificate_type,
}
}
Err(e) => ClientSimulationResult {
client_name: client.name.clone(),
client_id: client.short_id.clone(),
success: false,
protocol: None,
cipher: None,
error: Some(e.to_string()),
handshake_time_ms: None,
alpn: None,
key_exchange: None,
forward_secrecy: false,
certificate_type: None,
},
}
}
async fn try_connect_as_client(&self, client: &ClientProfile) -> Result<HandshakeInfo> {
use std::sync::Arc;
use tokio_rustls::TlsConnector;
let addr = self.target.socket_addrs()[0];
let stream = timeout(self.connect_timeout, TcpStream::connect(addr)).await??;
let config = self.build_client_config(client)?;
let connector = TlsConnector::from(Arc::new(config));
let hostname = self.target.hostname.clone();
let domain = rustls_pki_types::ServerName::try_from(hostname.as_str())
.map_err(|_| anyhow::anyhow!("Invalid DNS name"))?
.to_owned();
let tls_stream = timeout(self.read_timeout, connector.connect(domain, stream)).await??;
let (_io, connection) = tls_stream.into_inner();
let protocol = match connection.protocol_version() {
Some(rustls::ProtocolVersion::TLSv1_3) => Protocol::TLS13,
Some(rustls::ProtocolVersion::TLSv1_2) => Protocol::TLS12,
Some(rustls::ProtocolVersion::TLSv1_1) => Protocol::TLS11,
Some(rustls::ProtocolVersion::TLSv1_0) => Protocol::TLS10,
_ => Protocol::TLS12, };
let cipher_suite = connection.negotiated_cipher_suite().map(|cs| {
Self::format_cipher_suite(cs.suite())
});
let alpn = connection
.alpn_protocol()
.and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
let key_exchange = connection
.negotiated_cipher_suite()
.and_then(|cs| Self::extract_key_exchange(&cs, &client.curves));
let certificate_type = connection
.peer_certificates()
.and_then(|certs| certs.first())
.and_then(|cert| Self::extract_certificate_info(cert));
Ok(HandshakeInfo {
protocol,
cipher: cipher_suite,
alpn,
key_exchange,
certificate_type,
})
}
fn format_cipher_suite(suite: rustls::CipherSuite) -> String {
match suite {
rustls::CipherSuite::TLS13_AES_128_GCM_SHA256 => "TLS_AES_128_GCM_SHA256".to_string(),
rustls::CipherSuite::TLS13_AES_256_GCM_SHA384 => "TLS_AES_256_GCM_SHA384".to_string(),
rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 => {
"TLS_CHACHA20_POLY1305_SHA256".to_string()
}
_ => format!("{:?}", suite),
}
}
fn extract_key_exchange(
cipher_suite: &rustls::SupportedCipherSuite,
curves: &[String],
) -> Option<String> {
if matches!(
cipher_suite.suite(),
rustls::CipherSuite::TLS13_AES_128_GCM_SHA256
| rustls::CipherSuite::TLS13_AES_256_GCM_SHA384
| rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256
) {
let curve = curves.first().map(|c| c.as_str()).unwrap_or("x25519");
return Some(format!("ECDH {}", curve));
}
let suite_name = format!("{:?}", cipher_suite.suite());
if suite_name.contains("ECDHE") {
let curve = curves.first().map(|c| c.as_str()).unwrap_or("secp256r1");
Some(format!("ECDH {}", curve))
} else if suite_name.contains("DHE") {
Some("DH 2048".to_string())
} else if suite_name.contains("RSA") {
Some("RSA".to_string())
} else {
None
}
}
fn extract_certificate_info(cert: &rustls_pki_types::CertificateDer) -> Option<String> {
use x509_parser::prelude::*;
let parsed = X509Certificate::from_der(cert.as_ref());
if let Ok((_, cert)) = parsed {
let key_algo = cert.public_key().algorithm.algorithm.to_id_string();
let sig_algo = cert.signature_algorithm.algorithm.to_id_string();
let key_info = if key_algo.contains("rsaEncryption") {
let key_size = cert.public_key().subject_public_key.data.len() * 8;
format!("RSA {}", key_size)
} else if key_algo.contains("ecPublicKey") {
"ECDSA P-256".to_string()
} else {
"Unknown".to_string()
};
let hash = if sig_algo.contains("sha256") {
"SHA256"
} else if sig_algo.contains("sha384") {
"SHA384"
} else if sig_algo.contains("sha512") {
"SHA512"
} else {
"SHA1"
};
Some(format!("{} ({})", key_info, hash))
} else {
None
}
}
fn build_client_config(&self, client: &ClientProfile) -> Result<ClientConfig> {
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let versions = match client.highest_protocol.as_deref() {
Some("tls1_3") => vec![&rustls::version::TLS13],
Some("tls1_2") => vec![&rustls::version::TLS13, &rustls::version::TLS12],
Some("tls1_1") => vec![&rustls::version::TLS13, &rustls::version::TLS12],
Some("tls1") | Some("tls1_0") => vec![&rustls::version::TLS13, &rustls::version::TLS12],
_ => vec![&rustls::version::TLS13, &rustls::version::TLS12], };
let config = ClientConfig::builder_with_protocol_versions(&versions)
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(config)
}
pub async fn simulate_popular_clients(&self) -> Result<Vec<ClientSimulationResult>> {
let popular_ids = vec![
"chrome_120",
"firefox_120",
"safari_17_0",
"edge_120",
"android_14",
"ios_17_0",
];
let mut results = Vec::new();
for id in popular_ids {
if let Ok(result) = self.simulate_client_by_id(id).await {
results.push(result);
}
}
Ok(results)
}
}
impl ClientSimulationResult {
pub fn is_success(&self) -> bool {
self.success
}
pub fn summary(&self) -> String {
if self.success {
format!(
"{} - {} / {}",
self.client_name,
self.protocol
.as_ref()
.map(|p| p.to_string())
.unwrap_or_default(),
self.cipher.as_ref().unwrap_or(&"Unknown".to_string())
)
} else {
format!(
"{} - {}",
self.client_name,
self.error
.as_ref()
.unwrap_or(&"Connection failed".to_string())
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_simulation_result_summary() {
let result = ClientSimulationResult {
client_name: "Chrome 120".to_string(),
client_id: "chrome_120".to_string(),
success: true,
protocol: Some(Protocol::TLS13),
cipher: Some("TLS_AES_128_GCM_SHA256".to_string()),
error: None,
handshake_time_ms: Some(150),
alpn: Some("h2".to_string()),
key_exchange: Some("ECDH x25519".to_string()),
forward_secrecy: true,
certificate_type: Some("RSA 2048 (SHA256)".to_string()),
};
let summary = result.summary();
assert!(summary.contains("Chrome 120"));
assert!(summary.contains(""));
}
#[test]
fn test_failed_simulation_result() {
let result = ClientSimulationResult {
client_name: "Old Client".to_string(),
client_id: "old_client".to_string(),
success: false,
protocol: None,
cipher: None,
error: Some("TLS version not supported".to_string()),
handshake_time_ms: None,
alpn: None,
key_exchange: None,
forward_secrecy: false,
certificate_type: None,
};
let summary = result.summary();
assert!(summary.contains(""));
assert!(summary.contains("TLS version not supported"));
}
}