#![cfg(test)]
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone)]
pub enum TlsFailureFixture {
VersionMismatch {
client_max: String,
server_required: String,
},
CertificateError(String),
ProtocolMismatch,
HandshakeTimeout,
}
impl fmt::Display for TlsFailureFixture {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::VersionMismatch {
client_max,
server_required,
} => {
write!(
f,
"TLS version mismatch: client supports up to {}, server requires {}",
client_max, server_required
)
}
Self::CertificateError(msg) => write!(f, "TLS certificate error: {}", msg),
Self::ProtocolMismatch => write!(f, "TLS protocol negotiation failed"),
Self::HandshakeTimeout => write!(f, "TLS handshake timeout"),
}
}
}
#[derive(Debug)]
pub struct FailingTlsOtlpClient {
pub endpoint: String,
pub requests_attempted: Vec<String>,
pub tls_failures: Vec<TlsFailureFixture>,
pub should_fail_tls: bool,
pub failure_type: TlsFailureFixture,
}
impl FailingTlsOtlpClient {
fn new_with_tls_failure(endpoint: &str, failure_type: TlsFailureFixture) -> Self {
Self {
endpoint: endpoint.to_string(),
requests_attempted: Vec::new(),
tls_failures: Vec::new(),
should_fail_tls: true,
failure_type,
}
}
fn send_request(&mut self, request_body: &[u8]) -> Result<HttpResponseFixture, String> {
self.requests_attempted.push(format!(
"POST {} ({} bytes)",
self.endpoint,
request_body.len()
));
if self.should_fail_tls {
self.tls_failures.push(self.failure_type.clone());
let error_msg = format!("OTLP request failed: TLS error: {}", self.failure_type);
return Err(error_msg);
}
Ok(HttpResponseFixture {
status: 200,
headers: HashMap::new(),
body: b"".to_vec(),
})
}
fn send_request_with_forever_retry(
&mut self,
request_body: &[u8],
max_attempts: usize,
) -> Result<HttpResponseFixture, String> {
for attempt in 1..=max_attempts {
self.requests_attempted.push(format!(
"POST {} attempt {} ({} bytes)",
self.endpoint,
attempt,
request_body.len()
));
if self.should_fail_tls {
self.tls_failures.push(self.failure_type.clone());
println!("📊 Attempt {}: TLS handshake failed, retrying...", attempt);
continue; }
return Ok(HttpResponseFixture {
status: 200,
headers: HashMap::new(),
body: b"".to_vec(),
});
}
Err(format!(
"TLS handshake failed after {} attempts",
max_attempts
))
}
fn send_request_with_plaintext_fallback(
&mut self,
request_body: &[u8],
) -> Result<HttpResponseFixture, String> {
self.requests_attempted.push(format!(
"POST {} ({} bytes)",
self.endpoint,
request_body.len()
));
if self.should_fail_tls && self.endpoint.starts_with("https://") {
self.tls_failures.push(self.failure_type.clone());
println!("⚠️ TLS handshake failed, falling back to plaintext HTTP");
let http_endpoint = self.endpoint.replace("https://", "http://");
self.requests_attempted.push(format!(
"POST {} fallback ({} bytes)",
http_endpoint,
request_body.len()
));
return Ok(HttpResponseFixture {
status: 200,
headers: HashMap::new(),
body: b"".to_vec(),
});
}
Ok(HttpResponseFixture {
status: 200,
headers: HashMap::new(),
body: b"".to_vec(),
})
}
}
#[derive(Debug, Clone)]
pub struct HttpResponseFixture {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}
#[test]
fn audit_tls_version_mismatch_handling() {
println!("🔍 AUDIT: OTLP TLS version mismatch handling");
println!("📋 TLS version mismatch scenario:");
println!(" • Collector endpoint requires TLS 1.3 minimum");
println!(" • Client supports TLS 1.2 maximum");
println!(" • Handshake fails during version negotiation");
println!(" • Expected: Fail-fast with actionable error message");
let version_mismatch = TlsFailureFixture::VersionMismatch {
client_max: "TLS 1.2".to_string(),
server_required: "TLS 1.3".to_string(),
};
let mut client = FailingTlsOtlpClient::new_with_tls_failure(
"https://collector.example.com/v1/traces",
version_mismatch,
);
let test_payload = b"encoded OTLP protobuf payload";
println!("📊 Test scenario:");
println!(" Endpoint: {}", client.endpoint);
println!(" Client TLS: up to TLS 1.2");
println!(" Server requirement: TLS 1.3+");
println!("📊 Testing current behavior (fail-fast):");
let result = client.send_request(test_payload);
println!(" Result: {:?}", result);
println!(" Requests attempted: {}", client.requests_attempted.len());
println!(" TLS failures: {}", client.tls_failures.len());
assert!(result.is_err());
assert_eq!(client.requests_attempted.len(), 1);
assert_eq!(client.tls_failures.len(), 1);
let error_msg = result.unwrap_err();
assert!(error_msg.contains("TLS error"));
assert!(error_msg.contains("version mismatch"));
println!("✅ SOUND: Fails fast with TLS version mismatch error");
println!("📊 Testing defective retry-forever behavior:");
let mut retry_client = FailingTlsOtlpClient::new_with_tls_failure(
"https://collector.example.com/v1/traces",
TlsFailureFixture::VersionMismatch {
client_max: "TLS 1.2".to_string(),
server_required: "TLS 1.3".to_string(),
},
);
let retry_result = retry_client.send_request_with_forever_retry(test_payload, 5);
println!(" Retry result: {:?}", retry_result);
println!(
" Retry attempts: {}",
retry_client.requests_attempted.len()
);
println!(" TLS failures: {}", retry_client.tls_failures.len());
assert!(retry_result.is_err());
assert_eq!(retry_client.requests_attempted.len(), 5);
assert_eq!(retry_client.tls_failures.len(), 5);
println!("⚠️ DEFECTIVE: Retry forever wastes resources without resolution");
println!("🚨 AUDIT CONCLUSION: Current behavior is SOUND");
println!(" ✅ Fails fast on TLS version mismatch");
println!(" ✅ Does not retry forever");
println!(" ✅ Error message includes TLS context");
}
#[test]
fn audit_no_plaintext_downgrade_on_tls_failure() {
println!("🔍 AUDIT: OTLP TLS failure plaintext downgrade protection");
println!("📋 Security requirement:");
println!(" • HTTPS OTLP endpoints must never downgrade to HTTP");
println!(" • TLS failures should fail-fast, not fallback");
println!(" • Prevents accidental plaintext telemetry transmission");
println!(" • Maintains data confidentiality and integrity");
let tls_timeout = TlsFailureFixture::HandshakeTimeout;
println!("📊 Testing current behavior (no downgrade):");
let mut secure_client = FailingTlsOtlpClient::new_with_tls_failure(
"https://secure-collector.company.com/v1/traces",
tls_timeout.clone(),
);
let result = secure_client.send_request(b"sensitive telemetry data");
println!(" HTTPS result: {:?}", result);
println!(
" Requests attempted: {}",
secure_client.requests_attempted.len()
);
assert!(result.is_err());
assert_eq!(secure_client.requests_attempted.len(), 1);
assert!(secure_client.requests_attempted[0].contains("https://"));
println!("✅ SOUND: HTTPS endpoint failure does not trigger HTTP fallback");
println!("📊 Testing defective plaintext downgrade behavior:");
let mut insecure_client = FailingTlsOtlpClient::new_with_tls_failure(
"https://secure-collector.company.com/v1/traces",
tls_timeout,
);
let downgrade_result =
insecure_client.send_request_with_plaintext_fallback(b"sensitive telemetry data");
println!(" Downgrade result: {:?}", downgrade_result);
println!(
" Requests attempted: {}",
insecure_client.requests_attempted.len()
);
assert!(downgrade_result.is_ok()); assert_eq!(insecure_client.requests_attempted.len(), 2);
assert!(insecure_client.requests_attempted[0].contains("https://"));
assert!(insecure_client.requests_attempted[1].contains("http://"));
println!("⚠️ DEFECTIVE: Plaintext downgrade exposes sensitive telemetry data");
println!("🚨 SECURITY AUDIT: Current behavior is SOUND");
println!(" ✅ No plaintext downgrade on TLS failure");
println!(" ✅ Maintains HTTPS-only data transmission");
println!(" ✅ Fails closed for security");
}
#[test]
fn audit_tls_error_message_actionability() {
println!("🔍 AUDIT: OTLP TLS error message actionability");
println!("📋 Actionable error message requirements:");
println!(" • Identify TLS as the failure point");
println!(" • Indicate specific failure type when possible");
println!(" • Guide user toward configuration changes");
println!(" • Distinguish TLS from network/DNS failures");
let test_cases = vec![
(
"Version mismatch",
TlsFailureFixture::VersionMismatch {
client_max: "TLS 1.2".to_string(),
server_required: "TLS 1.3".to_string(),
},
),
(
"Certificate error",
TlsFailureFixture::CertificateError("certificate has expired".to_string()),
),
("Protocol mismatch", TlsFailureFixture::ProtocolMismatch),
("Handshake timeout", TlsFailureFixture::HandshakeTimeout),
];
println!("📊 Testing TLS error message quality:");
for (scenario, error_type) in test_cases {
println!(" Scenario: {}", scenario);
let mut client = FailingTlsOtlpClient::new_with_tls_failure(
"https://collector.example.com/v1/traces",
error_type,
);
let result = client.send_request(b"test payload");
if let Err(error_msg) = result {
println!(" Error: {}", error_msg);
assert!(
error_msg.contains("TLS error"),
"Should identify TLS as failure point"
);
assert!(
error_msg.contains("OTLP request failed"),
"Should indicate OTLP context"
);
let has_actionable_info = error_msg.contains("version")
|| error_msg.contains("certificate")
|| error_msg.contains("protocol")
|| error_msg.contains("timeout");
if !has_actionable_info {
println!(" ⚠️ Warning: Error message lacks specific diagnostic information");
} else {
println!(" ✅ Contains specific failure information");
}
}
}
println!("📊 Error message enhancement opportunities:");
println!(" • Current: Generic 'TLS error' prefix");
println!(" • Enhancement: Include TLS configuration guidance");
println!(" • Example: 'TLS error: version mismatch. Try upgrading client TLS version.'");
println!(" • Example: 'TLS error: certificate validation failed. Check CA configuration.'");
println!("✅ SOUND: TLS errors fail-fast with error context");
println!("📌 IMPROVEMENT: Error messages could include configuration guidance");
}
#[test]
fn audit_otlp_tls_error_classification() {
println!("🔍 AUDIT: OTLP TLS error classification in retry logic");
println!("📋 Current OTLP error handling (otel.rs):");
println!(" • Line 1077: .map_err(|e| OtlpError::non_retryable(...))");
println!(" • TLS errors from HttpClient become non-retryable");
println!(" • No retry attempts on TLS handshake failure");
println!(" • Consistent with OTLP best practice");
println!("📊 TLS error classification analysis:");
println!(" ✅ Version mismatch: non-retryable (correct - config change needed)");
println!(" ✅ Certificate error: non-retryable (correct - cert/CA fix needed)");
println!(" ✅ Protocol error: non-retryable (correct - protocol config needed)");
println!(" ✅ Handshake timeout: non-retryable (correct - not transient)");
println!("📊 Comparison with other error types:");
println!(" • 502/503/504: retryable (server-side, transient)");
println!(" • DNS errors: non-retryable (config issue)");
println!(" • TLS errors: non-retryable (config/compatibility issue)");
println!(" • Connection refused: retryable (service might restart)");
fn classify_otlp_error(error_type: &str) -> &'static str {
match error_type {
"TLS error" => "non_retryable", "502 Bad Gateway" => "retryable",
"503 Service Unavailable" => "retryable",
"DNS resolution failed" => "non_retryable",
_ => "unknown",
}
}
let error_types = [
"TLS error",
"502 Bad Gateway",
"503 Service Unavailable",
"DNS resolution failed",
];
println!("📊 OTLP error classification matrix:");
for error_type in error_types {
let classification = classify_otlp_error(error_type);
println!(" {}: {}", error_type, classification);
}
assert_eq!(classify_otlp_error("TLS error"), "non_retryable");
println!("✅ SOUND: TLS errors correctly classified as non-retryable");
println!(" • Prevents wasteful retry loops");
println!(" • Forces users to fix configuration issues");
println!(" • Aligned with OTLP specification guidance");
println!("🚨 AUDIT CONCLUSION: TLS handshake failure handling is SOUND");
println!(" Current: Fail-fast with clear TLS error context");
println!(" Security: No plaintext downgrade");
println!(" Performance: No wasteful retry loops");
}