#[derive(Debug, Clone)]
pub struct StealthTlsWireConfig {
pub tls12_cipher_suites: Vec<u16>,
pub tls13_cipher_suites: Vec<u16>,
pub signature_algorithms: Vec<u16>,
pub supported_groups: Vec<u16>,
pub alpn_protocols: Vec<Vec<u8>>,
pub h2_settings_payload: Vec<u8>,
pub h2_initial_stream_size: u32,
pub h2_initial_connection_window_size: u32,
pub h2_max_frame_size: u32,
pub h2_max_header_list_size: u32,
}
impl StealthTlsWireConfig {
pub fn from_profile(profile: &crate::StealthProfile) -> Self {
let tls = &profile.tls;
let http2 = &profile.http2;
let tls13_suites: Vec<u16> = tls
.cipher_suites
.iter()
.copied()
.filter(|s| (0x1301..=0x1303).contains(s))
.collect();
let tls12_suites: Vec<u16> = tls
.cipher_suites
.iter()
.copied()
.filter(|s| !(0x1301..=0x1303).contains(s))
.collect();
let h2_settings = http2.settings_frame_payload();
let mut h2_wire = Vec::with_capacity(h2_settings.len() * 6);
for (id, value) in &h2_settings {
h2_wire.extend_from_slice(&id.to_be_bytes());
h2_wire.extend_from_slice(&value.to_be_bytes());
}
let h2_conn_window = http2.initial_window_size;
StealthTlsWireConfig {
tls12_cipher_suites: tls12_suites,
tls13_cipher_suites: tls13_suites,
signature_algorithms: tls.signature_algorithms.clone(),
supported_groups: tls.supported_groups.clone(),
alpn_protocols: tls.alpn_protocols.clone(),
h2_settings_payload: h2_wire,
h2_initial_stream_size: http2.initial_window_size,
h2_initial_connection_window_size: h2_conn_window,
h2_max_frame_size: http2.max_frame_size,
h2_max_header_list_size: http2.max_header_list_size,
}
}
pub fn firefox() -> Self {
Self::from_profile(&crate::StealthProfile::firefox_default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_profile_firefox_nonempty() {
let profile = crate::StealthProfile::firefox_default();
let config = StealthTlsWireConfig::from_profile(&profile);
assert!(!config.tls12_cipher_suites.is_empty());
assert!(!config.tls13_cipher_suites.is_empty());
assert!(!config.signature_algorithms.is_empty());
assert!(!config.supported_groups.is_empty());
assert!(!config.alpn_protocols.is_empty());
assert!(!config.h2_settings_payload.is_empty());
}
#[test]
fn from_profile_chrome_nonempty() {
let profile = crate::StealthProfile::chrome_default();
let config = StealthTlsWireConfig::from_profile(&profile);
assert!(!config.tls12_cipher_suites.is_empty());
assert!(!config.alpn_protocols.is_empty());
}
#[test]
fn firefox_convenience_matches_from_profile() {
let from_convenience = StealthTlsWireConfig::firefox();
let from_explicit =
StealthTlsWireConfig::from_profile(&crate::StealthProfile::firefox_default());
assert_eq!(
from_convenience.tls12_cipher_suites,
from_explicit.tls12_cipher_suites,
);
assert_eq!(
from_convenience.tls13_cipher_suites,
from_explicit.tls13_cipher_suites,
);
assert_eq!(
from_convenience.alpn_protocols,
from_explicit.alpn_protocols
);
assert_eq!(
from_convenience.h2_initial_stream_size,
from_explicit.h2_initial_stream_size,
);
}
#[test]
fn tls13_suites_contain_standard_three() {
let config = StealthTlsWireConfig::firefox();
assert!(config.tls13_cipher_suites.contains(&0x1301)); assert!(config.tls13_cipher_suites.contains(&0x1302)); assert!(config.tls13_cipher_suites.contains(&0x1303)); }
#[test]
fn tls12_suites_do_not_contain_tls13_range() {
let config = StealthTlsWireConfig::firefox();
for suite in &config.tls12_cipher_suites {
assert!(
!(0x1301..=0x1303).contains(suite),
"TLS 1.2 suite list should not contain TLS 1.3 suite 0x{:04x}",
suite
);
}
}
#[test]
fn suite_partition_completeness() {
let config = StealthTlsWireConfig::firefox();
let profile = crate::StealthProfile::firefox_default();
let total = profile.tls.cipher_suites.len();
let tls13 = config.tls13_cipher_suites.len();
let tls12 = config.tls12_cipher_suites.len();
assert_eq!(total, tls13 + tls12);
}
#[test]
fn h2_settings_payload_wire_format() {
let config = StealthTlsWireConfig::firefox();
assert_eq!(config.h2_settings_payload.len(), 36);
assert_eq!(&config.h2_settings_payload[0..2], &[0x00, 0x01]);
}
#[test]
fn h2_initial_stream_size_firefox() {
let config = StealthTlsWireConfig::firefox();
assert_eq!(config.h2_initial_stream_size, 131072);
}
#[test]
fn h2_max_frame_size_firefox() {
let config = StealthTlsWireConfig::firefox();
assert_eq!(config.h2_max_frame_size, 16384);
}
#[test]
fn h2_max_header_list_size_firefox() {
let config = StealthTlsWireConfig::firefox();
assert_eq!(config.h2_max_header_list_size, 262144);
}
#[test]
fn alpn_protocols_contain_h2() {
let config = StealthTlsWireConfig::firefox();
assert!(config.alpn_protocols.iter().any(|p| p == b"h2"));
assert!(config.alpn_protocols.iter().any(|p| p == b"http/1.1"));
}
#[test]
fn firefox_and_chrome_different() {
let ff = StealthTlsWireConfig::firefox();
let ch = StealthTlsWireConfig::from_profile(&crate::StealthProfile::chrome_default());
assert_ne!(ff.tls12_cipher_suites.len(), ch.tls12_cipher_suites.len());
assert_ne!(ff.h2_initial_stream_size, ch.h2_initial_stream_size);
}
#[test]
fn clone_preserves_all_fields() {
let config = StealthTlsWireConfig::firefox();
let cloned = config.clone();
assert_eq!(config.tls12_cipher_suites, cloned.tls12_cipher_suites);
assert_eq!(config.tls13_cipher_suites, cloned.tls13_cipher_suites);
assert_eq!(config.alpn_protocols, cloned.alpn_protocols);
assert_eq!(config.h2_settings_payload, cloned.h2_settings_payload);
assert_eq!(config.h2_initial_stream_size, cloned.h2_initial_stream_size);
}
}