mirror of
https://github.com/telemt/telemt.git
synced 2026-06-11 21:41:43 +03:00
Replay-safe TLS-F ServerHello profile consistency
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -3937,10 +3937,20 @@ mod tests {
|
||||
session_id: Vec::new(),
|
||||
cipher_suite: [0x13, 0x01],
|
||||
compression: 0,
|
||||
extensions: vec![crate::tls_front::types::TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: vec![0x00, 0x1d, 0x00, 0x20],
|
||||
}],
|
||||
extensions: {
|
||||
let mut key_share = vec![0x00, 0x1d, 0x00, 0x20];
|
||||
key_share.resize(36, 0x42);
|
||||
vec![
|
||||
crate::tls_front::types::TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
crate::tls_front::types::TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: key_share,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
cert_info: None,
|
||||
cert_payload: Some(TlsCertPayload {
|
||||
@@ -3981,12 +3991,12 @@ mod tests {
|
||||
);
|
||||
assert!(
|
||||
output.contains(
|
||||
"telemt_tls_front_profile_server_hello_bytes{domain=\"primary.example\"} 52"
|
||||
"telemt_tls_front_profile_server_hello_bytes{domain=\"primary.example\"} 90"
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
output.contains(
|
||||
"telemt_tls_front_profile_server_hello_extensions{domain=\"primary.example\"} 1"
|
||||
"telemt_tls_front_profile_server_hello_extensions{domain=\"primary.example\"} 2"
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -364,6 +364,9 @@ impl TlsFrontCache {
|
||||
warn!(domain = %cached.domain, "Skipping stale TLS cache entry (>72h)");
|
||||
continue;
|
||||
}
|
||||
cached
|
||||
.behavior_profile
|
||||
.refresh_server_hello_summary(&cached.server_hello_template);
|
||||
let domain = cached.domain.clone();
|
||||
self.set(&domain, cached).await;
|
||||
loaded += 1;
|
||||
|
||||
@@ -21,6 +21,13 @@ const EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
|
||||
const EXT_KEY_SHARE: u16 = 0x0033;
|
||||
const EXT_ALPN: u16 = 0x0010;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FallbackShapeFamily {
|
||||
NginxLike,
|
||||
BoringSslLike,
|
||||
RustlsLike,
|
||||
}
|
||||
|
||||
fn parse_profiled_key_share_group(data: &[u8]) -> Option<u16> {
|
||||
if data.len() < 4 {
|
||||
return None;
|
||||
@@ -38,12 +45,26 @@ fn parse_profiled_key_share_group(data: &[u8]) -> Option<u16> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the origin-profiled ServerHello key_share group when it is replay-safe.
|
||||
pub(crate) fn profiled_server_hello_key_share_group(cached: &CachedTlsData) -> Option<u16> {
|
||||
if !matches!(
|
||||
fn effective_profiled_server_hello_record_len(cached: &CachedTlsData) -> usize {
|
||||
if cached.behavior_profile.server_hello_record_len == 0 {
|
||||
cached.server_hello_template.record_body_len()
|
||||
} else {
|
||||
cached.behavior_profile.server_hello_record_len
|
||||
}
|
||||
}
|
||||
|
||||
fn should_replay_profiled_server_hello_shape(cached: &CachedTlsData) -> bool {
|
||||
matches!(
|
||||
cached.behavior_profile.source,
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged
|
||||
) {
|
||||
) && cached.server_hello_template.is_replay_safe_tls13_shape(
|
||||
effective_profiled_server_hello_record_len(cached),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the origin-profiled ServerHello key_share group when it is replay-safe.
|
||||
pub(crate) fn profiled_server_hello_key_share_group(cached: &CachedTlsData) -> Option<u16> {
|
||||
if !should_replay_profiled_server_hello_shape(cached) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -105,6 +126,76 @@ fn ensure_payload_capacity(mut sizes: Vec<usize>, payload_len: usize) -> Vec<usi
|
||||
sizes
|
||||
}
|
||||
|
||||
fn fallback_shape_family(cached: &CachedTlsData) -> FallbackShapeFamily {
|
||||
match cached.behavior_profile.source {
|
||||
TlsProfileSource::Rustls => FallbackShapeFamily::RustlsLike,
|
||||
TlsProfileSource::Default => {
|
||||
let mut hasher = Hasher::new();
|
||||
hasher.update(cached.domain.as_bytes());
|
||||
hasher.update(&cached.total_app_data_len.to_le_bytes());
|
||||
if hasher.finalize() & 1 == 0 {
|
||||
FallbackShapeFamily::NginxLike
|
||||
} else {
|
||||
FallbackShapeFamily::BoringSslLike
|
||||
}
|
||||
}
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged => FallbackShapeFamily::NginxLike,
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_total_app_data_len(cached: &CachedTlsData) -> usize {
|
||||
cached
|
||||
.total_app_data_len
|
||||
.max(cached.app_data_records_sizes.iter().sum())
|
||||
.max(1024)
|
||||
}
|
||||
|
||||
fn push_fallback_size(sizes: &mut Vec<usize>, size: usize) {
|
||||
sizes.push(size.clamp(MIN_APP_DATA, MAX_APP_DATA));
|
||||
}
|
||||
|
||||
fn fallback_family_app_data_sizes(cached: &CachedTlsData) -> Vec<usize> {
|
||||
if matches!(cached.behavior_profile.source, TlsProfileSource::Rustls)
|
||||
&& !cached.app_data_records_sizes.is_empty()
|
||||
{
|
||||
return cached.app_data_records_sizes.clone();
|
||||
}
|
||||
|
||||
let family = fallback_shape_family(cached);
|
||||
let mut remaining = fallback_total_app_data_len(cached);
|
||||
let preferred_chunk = match family {
|
||||
FallbackShapeFamily::NginxLike => 2896,
|
||||
FallbackShapeFamily::BoringSslLike => 1369,
|
||||
FallbackShapeFamily::RustlsLike => 2048,
|
||||
};
|
||||
let split_threshold = match family {
|
||||
FallbackShapeFamily::NginxLike => 4096,
|
||||
FallbackShapeFamily::BoringSslLike => 1536,
|
||||
FallbackShapeFamily::RustlsLike => 3072,
|
||||
};
|
||||
|
||||
if remaining <= split_threshold {
|
||||
return vec![remaining.clamp(MIN_APP_DATA, MAX_APP_DATA)];
|
||||
}
|
||||
|
||||
let mut sizes: Vec<usize> = Vec::new();
|
||||
while remaining > 0 {
|
||||
let chunk = remaining.min(preferred_chunk).min(MAX_APP_DATA);
|
||||
if chunk < MIN_APP_DATA {
|
||||
if let Some(last) = sizes.last_mut() {
|
||||
*last = (*last).saturating_add(chunk).min(MAX_APP_DATA);
|
||||
} else {
|
||||
push_fallback_size(&mut sizes, chunk);
|
||||
}
|
||||
break;
|
||||
}
|
||||
push_fallback_size(&mut sizes, chunk);
|
||||
remaining = remaining.saturating_sub(chunk);
|
||||
}
|
||||
|
||||
sizes
|
||||
}
|
||||
|
||||
fn emulated_app_data_sizes(cached: &CachedTlsData) -> Vec<usize> {
|
||||
match cached.behavior_profile.source {
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged => {
|
||||
@@ -116,14 +207,10 @@ fn emulated_app_data_sizes(cached: &CachedTlsData) -> Vec<usize> {
|
||||
}
|
||||
return vec![cached.total_app_data_len.max(1024)];
|
||||
}
|
||||
TlsProfileSource::Default | TlsProfileSource::Rustls => {}
|
||||
TlsProfileSource::Default | TlsProfileSource::Rustls => {
|
||||
return fallback_family_app_data_sizes(cached);
|
||||
}
|
||||
}
|
||||
|
||||
let mut sizes = cached.app_data_records_sizes.clone();
|
||||
if sizes.is_empty() {
|
||||
sizes.push(cached.total_app_data_len.max(1024));
|
||||
}
|
||||
sizes
|
||||
}
|
||||
|
||||
fn emulated_change_cipher_spec_count(_cached: &CachedTlsData) -> usize {
|
||||
@@ -151,7 +238,13 @@ fn emulated_ticket_record_sizes(
|
||||
sizes.extend(profiled_sizes.iter().copied().take(target_count));
|
||||
|
||||
while sizes.len() < target_count {
|
||||
sizes.push(rng.range(48) + 48);
|
||||
let family = fallback_shape_family(cached);
|
||||
let base = match family {
|
||||
FallbackShapeFamily::NginxLike => 96,
|
||||
FallbackShapeFamily::BoringSslLike => 80,
|
||||
FallbackShapeFamily::RustlsLike => 112,
|
||||
};
|
||||
sizes.push(base + rng.range(64));
|
||||
}
|
||||
|
||||
sizes
|
||||
@@ -287,22 +380,24 @@ fn build_profiled_server_hello_extensions(
|
||||
let mut saw_supported_versions = false;
|
||||
let mut saw_key_share = false;
|
||||
|
||||
for ext in &cached.server_hello_template.extensions {
|
||||
replay_profiled_server_hello_extension(
|
||||
ext,
|
||||
&mut extensions,
|
||||
server_key_share,
|
||||
&mut saw_supported_versions,
|
||||
&mut saw_key_share,
|
||||
);
|
||||
if should_replay_profiled_server_hello_shape(cached) {
|
||||
for ext in &cached.server_hello_template.extensions {
|
||||
replay_profiled_server_hello_extension(
|
||||
ext,
|
||||
&mut extensions,
|
||||
server_key_share,
|
||||
&mut saw_supported_versions,
|
||||
&mut saw_key_share,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !saw_key_share {
|
||||
push_key_share_extension(&mut extensions, server_key_share);
|
||||
}
|
||||
if !saw_supported_versions {
|
||||
push_supported_versions_extension(&mut extensions);
|
||||
}
|
||||
if !saw_key_share {
|
||||
push_key_share_extension(&mut extensions, server_key_share);
|
||||
}
|
||||
|
||||
extensions
|
||||
}
|
||||
@@ -594,10 +689,16 @@ mod tests {
|
||||
fn profiled_server_hello_key_share_group_reads_raw_x25519_profile() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.behavior_profile.source = TlsProfileSource::Raw;
|
||||
cached.server_hello_template.extensions = vec![TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: server_key_share_extension_data(TLS_NAMED_GROUP_X25519, 32),
|
||||
}];
|
||||
cached.server_hello_template.extensions = vec![
|
||||
TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: server_key_share_extension_data(TLS_NAMED_GROUP_X25519, 32),
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
profiled_server_hello_key_share_group(&cached),
|
||||
@@ -711,6 +812,82 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_replays_safe_raw_extension_order() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.behavior_profile.source = TlsProfileSource::Raw;
|
||||
cached.server_hello_template.extensions = vec![
|
||||
TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: server_key_share_extension_data(TLS_NAMED_GROUP_X25519, 32),
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
];
|
||||
let rng = SecureRandom::new();
|
||||
let response = build_emulated_server_hello(
|
||||
b"secret",
|
||||
&[0x21; 32],
|
||||
&[0x22; 16],
|
||||
&cached,
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
server_hello_extension_types(&response),
|
||||
vec![0x0033, 0x002b]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_uses_canonical_order_for_unsafe_raw_shape() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.behavior_profile.source = TlsProfileSource::Raw;
|
||||
cached.server_hello_template.extensions = vec![
|
||||
TlsExtension {
|
||||
ext_type: 0x0010,
|
||||
data: vec![0x00, 0x03, 0x02, b'h', b'2'],
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: server_key_share_extension_data(TLS_NAMED_GROUP_X25519, 32),
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
];
|
||||
let rng = SecureRandom::new();
|
||||
let response = build_emulated_server_hello(
|
||||
b"secret",
|
||||
&[0x21; 32],
|
||||
&[0x22; 16],
|
||||
&cached,
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
server_hello_extension_types(&response),
|
||||
vec![0x002b, 0x0033]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_random_fallback_when_no_cert_payload() {
|
||||
let cached = make_cached(None);
|
||||
|
||||
@@ -1477,7 +1477,8 @@ pub async fn fetch_real_tls_with_strategy(
|
||||
raw.cert_info = rustls.cert_info;
|
||||
raw.cert_payload = rustls.cert_payload;
|
||||
raw.behavior_profile.source = TlsProfileSource::Merged;
|
||||
raw.behavior_profile.refresh_quality();
|
||||
raw.behavior_profile
|
||||
.refresh_server_hello_summary(&raw.server_hello_parsed);
|
||||
debug!(sni = %sni, "Fetched TLS metadata via adaptive raw probe + rustls cert chain");
|
||||
Ok(raw)
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::SystemTime;
|
||||
|
||||
const EXT_ALPN: u16 = 0x0010;
|
||||
const EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
|
||||
const EXT_KEY_SHARE: u16 = 0x0033;
|
||||
const TLS_LEGACY_SERVER_HELLO_VERSION: [u8; 2] = [0x03, 0x03];
|
||||
const TLS_VERSION_13: [u8; 2] = [0x03, 0x04];
|
||||
const TLS_NAMED_GROUP_X25519: u16 = 0x001d;
|
||||
const TLS_NAMED_GROUP_X25519MLKEM768: u16 = 0x11ec;
|
||||
|
||||
@@ -50,6 +54,50 @@ impl ParsedServerHello {
|
||||
.find(|extension| extension.ext_type == EXT_KEY_SHARE)
|
||||
.and_then(|extension| parse_key_share_group(&extension.data))
|
||||
}
|
||||
|
||||
/// Return true when the cached ServerHello can safely drive visible TLS 1.3 replay.
|
||||
pub(crate) fn is_replay_safe_tls13_shape(&self, record_body_len: usize) -> bool {
|
||||
if self.version != TLS_LEGACY_SERVER_HELLO_VERSION
|
||||
|| self.compression != 0
|
||||
|| self.session_id.len() > 32
|
||||
|| !is_supported_tls13_cipher_suite(self.cipher_suite)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if record_body_len != 0 && record_body_len != self.record_body_len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut saw_supported_versions = false;
|
||||
let mut saw_key_share = false;
|
||||
for extension in &self.extensions {
|
||||
match extension.ext_type {
|
||||
EXT_SUPPORTED_VERSIONS => {
|
||||
if saw_supported_versions || extension.data.as_slice() != TLS_VERSION_13 {
|
||||
return false;
|
||||
}
|
||||
saw_supported_versions = true;
|
||||
}
|
||||
EXT_KEY_SHARE => {
|
||||
if saw_key_share || parse_key_share_group(&extension.data).is_none() {
|
||||
return false;
|
||||
}
|
||||
saw_key_share = true;
|
||||
}
|
||||
EXT_ALPN => {
|
||||
return false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
saw_supported_versions && saw_key_share
|
||||
}
|
||||
}
|
||||
|
||||
fn is_supported_tls13_cipher_suite(cipher_suite: [u8; 2]) -> bool {
|
||||
matches!(u16::from_be_bytes(cipher_suite), 0x1301 | 0x1302 | 0x1303)
|
||||
}
|
||||
|
||||
fn parse_key_share_group(data: &[u8]) -> Option<u16> {
|
||||
@@ -168,25 +216,29 @@ impl Default for TlsBehaviorProfile {
|
||||
impl TlsBehaviorProfile {
|
||||
/// Refresh cached visible ServerHello summary fields and quality.
|
||||
pub(crate) fn refresh_server_hello_summary(&mut self, server_hello: &ParsedServerHello) {
|
||||
let mut has_replay_safe_server_hello = false;
|
||||
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();
|
||||
has_replay_safe_server_hello =
|
||||
server_hello.is_replay_safe_tls13_shape(self.server_hello_record_len);
|
||||
} else {
|
||||
self.server_hello_record_len = 0;
|
||||
self.server_hello_extension_types.clear();
|
||||
self.server_hello_key_share_group = None;
|
||||
}
|
||||
|
||||
self.refresh_quality();
|
||||
self.refresh_quality(has_replay_safe_server_hello);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
fn refresh_quality(&mut self, has_replay_safe_server_hello: bool) {
|
||||
let has_raw_server_hello =
|
||||
matches!(self.source, TlsProfileSource::Raw | TlsProfileSource::Merged)
|
||||
&& has_replay_safe_server_hello;
|
||||
self.quality = if has_raw_server_hello && !self.app_data_record_sizes.is_empty() {
|
||||
TlsProfileQuality::RawStrict
|
||||
} else if has_raw_server_hello {
|
||||
@@ -233,6 +285,34 @@ pub struct TlsFetchResult {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tls13_key_share_extension() -> TlsExtension {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&TLS_NAMED_GROUP_X25519.to_be_bytes());
|
||||
data.extend_from_slice(&32u16.to_be_bytes());
|
||||
data.resize(36, 0x42);
|
||||
TlsExtension {
|
||||
ext_type: EXT_KEY_SHARE,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
fn replay_safe_server_hello() -> ParsedServerHello {
|
||||
ParsedServerHello {
|
||||
version: TLS_LEGACY_SERVER_HELLO_VERSION,
|
||||
random: [0u8; 32],
|
||||
session_id: vec![0x11; 32],
|
||||
cipher_suite: [0x13, 0x01],
|
||||
compression: 0,
|
||||
extensions: vec![
|
||||
TlsExtension {
|
||||
ext_type: EXT_SUPPORTED_VERSIONS,
|
||||
data: TLS_VERSION_13.to_vec(),
|
||||
},
|
||||
tls13_key_share_extension(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_tls_data_deserializes_without_behavior_profile() {
|
||||
let json = r#"
|
||||
@@ -260,4 +340,57 @@ mod tests {
|
||||
assert_eq!(cached.behavior_profile.source, TlsProfileSource::Default);
|
||||
assert_eq!(cached.behavior_profile.quality, TlsProfileQuality::Fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_safe_raw_server_hello_with_app_data_is_raw_strict() {
|
||||
let server_hello = replay_safe_server_hello();
|
||||
let mut behavior = TlsBehaviorProfile {
|
||||
source: TlsProfileSource::Raw,
|
||||
app_data_record_sizes: vec![1200],
|
||||
..TlsBehaviorProfile::default()
|
||||
};
|
||||
|
||||
behavior.refresh_server_hello_summary(&server_hello);
|
||||
|
||||
assert_eq!(behavior.quality, TlsProfileQuality::RawStrict);
|
||||
assert_eq!(
|
||||
behavior.server_hello_extension_types,
|
||||
vec![EXT_SUPPORTED_VERSIONS, EXT_KEY_SHARE]
|
||||
);
|
||||
assert_eq!(
|
||||
behavior.server_hello_key_share_group,
|
||||
Some(TLS_NAMED_GROUP_X25519)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_safe_raw_server_hello_without_app_data_is_raw_partial() {
|
||||
let server_hello = replay_safe_server_hello();
|
||||
let mut behavior = TlsBehaviorProfile {
|
||||
source: TlsProfileSource::Raw,
|
||||
..TlsBehaviorProfile::default()
|
||||
};
|
||||
|
||||
behavior.refresh_server_hello_summary(&server_hello);
|
||||
|
||||
assert_eq!(behavior.quality, TlsProfileQuality::RawPartial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_raw_server_hello_is_fallback_quality() {
|
||||
let mut server_hello = replay_safe_server_hello();
|
||||
server_hello.extensions.push(TlsExtension {
|
||||
ext_type: EXT_ALPN,
|
||||
data: vec![0x00, 0x03, 0x02, b'h', b'2'],
|
||||
});
|
||||
let mut behavior = TlsBehaviorProfile {
|
||||
source: TlsProfileSource::Raw,
|
||||
app_data_record_sizes: vec![1200],
|
||||
..TlsBehaviorProfile::default()
|
||||
};
|
||||
|
||||
behavior.refresh_server_hello_summary(&server_hello);
|
||||
|
||||
assert_eq!(behavior.quality, TlsProfileQuality::Fallback);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user