#![cfg(feature = "tls")]
use flowscope::tls::{
EchState, TlsClientHello, TlsHandshake,
ech::{classify_client_hello, ech_cover_domains, is_ech_cover_domain},
handshake::EchOutcome,
};
fn ch_with(present: bool, sni: Option<&str>) -> TlsClientHello {
let mut ch = TlsClientHello::default();
ch.ech_present = present;
ch.sni = sni.map(str::to_string);
ch
}
fn hs_with(outcome: EchOutcome, sni: Option<&str>) -> TlsHandshake {
let mut hs = TlsHandshake::default();
hs.ech_outcome = outcome;
hs.sni = sni.map(str::to_string);
hs
}
#[test]
fn client_hello_ext_absent_is_not_present() {
assert_eq!(
ch_with(false, Some("example.com")).ech_state(),
EchState::NotPresent
);
}
#[test]
fn client_hello_ext_present_known_cover_is_likely_real() {
assert_eq!(
ch_with(true, Some("a.cloudflare-ech.com")).ech_state(),
EchState::LikelyReal
);
}
#[test]
fn client_hello_ext_present_normal_sni_is_likely_grease() {
assert_eq!(
ch_with(true, Some("example.com")).ech_state(),
EchState::LikelyGrease
);
}
#[test]
fn client_hello_ext_present_no_sni_is_likely_grease() {
assert_eq!(ch_with(true, None).ech_state(), EchState::LikelyGrease);
}
#[test]
fn handshake_not_offered_is_not_present() {
assert_eq!(
hs_with(EchOutcome::NotOffered, Some("example.com")).ech_state(),
EchState::NotPresent
);
}
#[test]
fn handshake_rejected_is_rejected() {
assert_eq!(
hs_with(EchOutcome::Rejected, Some("example.com")).ech_state(),
EchState::Rejected
);
}
#[test]
fn handshake_accepted_with_cover_sni_is_likely_real() {
assert_eq!(
hs_with(EchOutcome::Accepted, Some("cloudflare-ech.com")).ech_state(),
EchState::LikelyReal
);
}
#[test]
fn handshake_accepted_with_non_cover_sni_is_likely_grease() {
assert_eq!(
hs_with(EchOutcome::Accepted, Some("example.com")).ech_state(),
EchState::LikelyGrease
);
}
#[test]
fn handshake_unknown_falls_through_to_sni_classification() {
assert_eq!(
hs_with(EchOutcome::Unknown, Some("public.cloudflare-ech.com")).ech_state(),
EchState::LikelyReal
);
}
#[test]
fn slug_vocabulary_locked() {
assert_eq!(EchState::NotPresent.as_str(), "not_present");
assert_eq!(EchState::LikelyReal.as_str(), "likely_real");
assert_eq!(EchState::LikelyGrease.as_str(), "likely_grease");
assert_eq!(EchState::Rejected.as_str(), "rejected");
}
#[test]
fn cover_domains_contains_cloudflare() {
assert!(
ech_cover_domains().contains(&"cloudflare-ech.com"),
"Cloudflare cover domain must be in the curated list"
);
}
#[test]
fn classify_client_hello_function_matches_method() {
for (present, sni) in [
(false, None),
(false, Some("example.com")),
(true, None),
(true, Some("example.com")),
(true, Some("cloudflare-ech.com")),
] {
assert_eq!(
ch_with(present, sni).ech_state(),
classify_client_hello(present, sni),
"mismatch for (present={present}, sni={sni:?})"
);
}
}
#[test]
fn cover_domain_predicate_rejects_substring_attack() {
assert!(!is_ech_cover_domain("cloudflare-ech.com.attacker.tld"));
assert!(!is_ech_cover_domain("fakecloudflare-ech.com"));
}