From a69d46b9c8cf40bece24c39d72f8214ebc638879 Mon Sep 17 00:00:00 2001 From: nikhilbirmiwal-pplx Date: Tue, 20 Oct 2026 14:04:36 +0000 Subject: [PATCH 1/2] net-gateway: tag TLS handshake failures with the peer's alert gateway.tls.handshake carried an `alert` tag that was always `none`, so a guest rejecting the interception chain was indistinguishable from a guest that disconnected or reset mid-handshake: both were `reason:io`. Read the fatal alert rustls reports as `Error::AlertReceived` out of the tokio-rustls I/O error and tag it with its IANA TLS Alerts registry name (`unknown_ca`, `bad_certificate`, ...), keyed by wire value. Codepoints the registry does not assign are tagged with their decimal value. The same name now fills the `tls_alert` log field on handshake and established-connection failures. `reason` is unchanged. --- crates/libs/metrics/src/gateway.rs | 4 + .../net/gateway/server/src/tcp_proxy/tls.rs | 96 +++++++++-- .../gateway/server/src/tcp_proxy/tls/tests.rs | 159 ++++++++++++++++++ 3 files changed, 249 insertions(+), 10 deletions(-) diff --git a/crates/libs/metrics/src/gateway.rs b/crates/libs/metrics/src/gateway.rs index fd00cbfcb..561835c7f 100644 --- a/crates/libs/metrics/src/gateway.rs +++ b/crates/libs/metrics/src/gateway.rs @@ -698,6 +698,10 @@ pub fn certificate_cache_entries(entries: usize, capacity: usize) { } /// One completed TLS handshake operation. `leg` names the two peers directly. +/// `alert` is the fatal alert the peer on that leg sent, named as in the IANA +/// TLS Alerts registry (`unknown_ca`, `bad_certificate`, ...), or `none` when +/// it sent none. On `client_to_netgw`, a certificate alert means the guest +/// rejected the interception chain; a disconnect or reset carries no alert. pub fn tls_handshake(leg: &str, outcome: &str, reason: &str, alert: &str) { counter( "gateway.tls.handshake", diff --git a/crates/libs/net/gateway/server/src/tcp_proxy/tls.rs b/crates/libs/net/gateway/server/src/tcp_proxy/tls.rs index dde96adbb..48f65ddcc 100644 --- a/crates/libs/net/gateway/server/src/tcp_proxy/tls.rs +++ b/crates/libs/net/gateway/server/src/tcp_proxy/tls.rs @@ -1,5 +1,6 @@ //! Verified TLS transport and ALPN. Callers authorize before establishing the origin. +use std::borrow::Cow; use std::io; use std::pin::Pin; use std::sync::Arc; @@ -63,7 +64,7 @@ impl TlsTransportError { outcome = self.as_str(), reason = failure.reason, io_kind = ?io_kind, - tls_alert = failure.alert, + tls_alert = failure.alert_field().as_deref(), certificate_validation_reason = failure.certificate, "inspected connection failed" ); @@ -103,7 +104,7 @@ impl TlsTransportError { Self::Protocol => TlsFailure::new("alpn_mismatch"), Self::Certificate(_) => TlsFailure::new("certificate_material"), Self::Configuration(_) => TlsFailure::new("configuration"), - Self::Downstream(_) | Self::Upstream(_) => TlsFailure::new("io"), + Self::Downstream(error) | Self::Upstream(error) => TlsFailure::io(error), Self::Connect(transport::Error::Timeout) => TlsFailure::new("timeout"), Self::Connect(transport::Error::Io(_)) => TlsFailure::new("io"), } @@ -113,9 +114,10 @@ impl TlsTransportError { #[derive(Clone, Copy)] struct TlsFailure { reason: &'static str, - alert: Option<&'static str>, - /// Bounded alert class for metrics. The full `alert` name stays in logs. - alert_class: &'static str, + /// The fatal alert the peer on this leg sent before the failure, if any. + /// A guest that rejects the interception chain sends one (`unknown_ca`, + /// `bad_certificate`, ...); a guest that disconnects or resets does not. + alert: Option, certificate: Option<&'static str>, } @@ -124,10 +126,84 @@ impl TlsFailure { Self { reason, alert: None, - alert_class: "none", certificate: None, } } + + fn io(error: &io::Error) -> Self { + Self { + alert: received_alert(error), + ..Self::new("io") + } + } + + /// The `alert` metric tag and log field value. `none` when the peer sent + /// no alert. + fn alert_tag(&self) -> Cow<'static, str> { + self.alert.map_or(Cow::Borrowed("none"), alert_name) + } + + fn alert_field(&self) -> Option> { + self.alert.map(alert_name) + } +} + +/// The fatal alert the peer sent, if this I/O error carries one. tokio-rustls +/// reports a TLS failure as an `io::Error` wrapping the `rustls::Error`, and +/// rustls reports a received fatal alert as `Error::AlertReceived`. +fn received_alert(error: &io::Error) -> Option { + match error.get_ref()?.downcast_ref::()? { + rustls::Error::AlertReceived(alert) => Some(*alert), + _ => None, + } +} + +/// The alert's description as named in the IANA TLS Alerts registry +/// (RFC 8446 section 6 and its updates), keyed by the wire value so the name +/// does not depend on how rustls spells its variants. A value the registry +/// does not assign is reported as its decimal wire value; the tag stays +/// bounded by the one-byte field. +fn alert_name(alert: rustls::AlertDescription) -> Cow<'static, str> { + let name = match u8::from(alert) { + 0 => "close_notify", + 10 => "unexpected_message", + 20 => "bad_record_mac", + 21 => "decryption_failed_RESERVED", + 22 => "record_overflow", + 30 => "decompression_failure_RESERVED", + 40 => "handshake_failure", + 41 => "no_certificate_RESERVED", + 42 => "bad_certificate", + 43 => "unsupported_certificate", + 44 => "certificate_revoked", + 45 => "certificate_expired", + 46 => "certificate_unknown", + 47 => "illegal_parameter", + 48 => "unknown_ca", + 49 => "access_denied", + 50 => "decode_error", + 51 => "decrypt_error", + 52 => "too_many_cids_requested", + 60 => "export_restriction_RESERVED", + 70 => "protocol_version", + 71 => "insufficient_security", + 80 => "internal_error", + 86 => "inappropriate_fallback", + 90 => "user_canceled", + 100 => "no_renegotiation_RESERVED", + 109 => "missing_extension", + 110 => "unsupported_extension", + 111 => "certificate_unobtainable_RESERVED", + 112 => "unrecognized_name", + 113 => "bad_certificate_status_response", + 114 => "bad_certificate_hash_value_RESERVED", + 115 => "unknown_psk_identity", + 116 => "certificate_required", + 120 => "no_application_protocol", + 121 => "ech_required", + unassigned => return Cow::Owned(unassigned.to_string()), + }; + Cow::Borrowed(name) } fn negotiated(alpn: Option<&[u8]>) -> Result { @@ -435,7 +511,7 @@ impl HandshakeAttempt { leg.as_str(), outcome, failure.reason, - failure.alert_class, + &failure.alert_tag(), ); tracing::warn!( telemetry_type = "tls_handshake_error", @@ -444,7 +520,7 @@ impl HandshakeAttempt { outcome, reason = failure.reason, io_kind = ?error.io_error().map(io::Error::kind), - tls_alert = failure.alert, + tls_alert = failure.alert_field().as_deref(), certificate_validation_reason = failure.certificate, "TLS handshake failed" ); @@ -488,7 +564,7 @@ impl TlsIo { if self.error_reported { return; } - let failure = TlsFailure::new("io"); + let failure = TlsFailure::io(error); self.error_reported = true; pplx_space_metrics::gateway::tls_connection_error(self.leg.as_str(), failure.reason); tracing::warn!( @@ -498,7 +574,7 @@ impl TlsIo { operation, reason = failure.reason, io_kind = ?error.kind(), - tls_alert = failure.alert, + tls_alert = failure.alert_field().as_deref(), certificate_validation_reason = failure.certificate, "established TLS connection failed" ); diff --git a/crates/libs/net/gateway/server/src/tcp_proxy/tls/tests.rs b/crates/libs/net/gateway/server/src/tcp_proxy/tls/tests.rs index ea09ac05c..3790d6bb7 100644 --- a/crates/libs/net/gateway/server/src/tcp_proxy/tls/tests.rs +++ b/crates/libs/net/gateway/server/src/tcp_proxy/tls/tests.rs @@ -253,3 +253,162 @@ mod http_failure { assert_eq!(http_reason(&error), "parse"); } } + +mod peer_alert { + use std::io; + use std::sync::Arc; + + use rustls::AlertDescription; + use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, ServerName}; + use tokio::io::{AsyncWriteExt, DuplexStream}; + use tokio_rustls::{TlsAcceptor, TlsConnector}; + + use super::super::{HandshakeAttempt, TlsFailure, TlsTransportError, alert_name}; + use crate::tls_interception::TlsHandshakeLeg; + + const HOST: &str = "origin.example"; + + fn provider() -> Arc { + Arc::new(rustls::crypto::aws_lc_rs::default_provider()) + } + + fn acceptor() -> TlsAcceptor { + let certified = rcgen::generate_simple_self_signed(vec![HOST.to_owned()]).unwrap(); + let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from( + certified.signing_key.serialize_der(), + )); + let config = rustls::ServerConfig::builder_with_provider(provider()) + .with_safe_default_protocol_versions() + .unwrap() + .with_no_client_auth() + .with_single_cert(vec![certified.cert.der().clone()], key) + .unwrap(); + TlsAcceptor::from(Arc::new(config)) + } + + /// A client whose trust store does not contain the server's issuer, as a + /// guest runtime with its own CA bundle sees the interception chain. + fn untrusting_connector() -> TlsConnector { + let config = rustls::ClientConfig::builder_with_provider(provider()) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(); + TlsConnector::from(Arc::new(config)) + } + + async fn accept_error(client: impl AsyncFnOnce(DuplexStream)) -> io::Error { + let (server, client_stream) = tokio::io::duplex(64 * 1024); + let (accepted, ()) = tokio::join!(acceptor().accept(server), client(client_stream)); + let Err(error) = accepted else { + panic!("handshake must fail"); + }; + error + } + + fn handshake_tags(error: &TlsTransportError) -> Vec { + let capture = pplx_space_metrics::test_capture::capture_counters_guard(); + HandshakeAttempt::record_failure(TlsHandshakeLeg::ClientToNetgw, error); + let metrics = capture.finish(); + let handshake = metrics + .iter() + .find(|metric| metric.name == "gateway.tls.handshake") + .expect("handshake counter"); + handshake.tags.clone() + } + + /// The case interception rollout must see: the guest refuses the chain and + /// says why. The gateway records the guest's alert under its registry name. + #[tokio::test] + async fn guest_rejecting_the_chain_is_tagged_with_its_alert() { + let error = accept_error(async |stream| { + let name = ServerName::try_from(HOST).unwrap(); + let Err(_) = untrusting_connector().connect(name, stream).await else { + panic!("guest must refuse the chain"); + }; + }) + .await; + let error = TlsTransportError::Downstream(error); + + assert_eq!(error.failure().alert, Some(AlertDescription::UnknownCA)); + let tags = handshake_tags(&error); + assert!(tags.contains(&"alert:unknown_ca".to_owned()), "{tags:?}"); + assert!(tags.contains(&"reason:io".to_owned()), "{tags:?}"); + } + + /// A guest that goes away mid-handshake sent no alert, and must not be + /// counted as rejecting the chain. + #[tokio::test] + async fn guest_disconnecting_is_tagged_without_an_alert() { + let error = accept_error(async |mut stream| { + stream.shutdown().await.unwrap(); + }) + .await; + let error = TlsTransportError::Downstream(error); + + assert_eq!(error.failure().alert, None); + assert!(handshake_tags(&error).contains(&"alert:none".to_owned())); + } + + #[test] + fn alert_names_follow_the_iana_registry() { + for (alert, name) in [ + (AlertDescription::CloseNotify, "close_notify"), + (AlertDescription::HandshakeFailure, "handshake_failure"), + (AlertDescription::BadCertificate, "bad_certificate"), + ( + AlertDescription::UnsupportedCertificate, + "unsupported_certificate", + ), + (AlertDescription::CertificateExpired, "certificate_expired"), + (AlertDescription::CertificateUnknown, "certificate_unknown"), + (AlertDescription::UnknownCA, "unknown_ca"), + (AlertDescription::DecryptError, "decrypt_error"), + (AlertDescription::ProtocolVersion, "protocol_version"), + (AlertDescription::UnrecognisedName, "unrecognized_name"), + ( + AlertDescription::NoApplicationProtocol, + "no_application_protocol", + ), + ( + AlertDescription::EncryptedClientHelloRequired, + "ech_required", + ), + (AlertDescription::Unknown(255), "255"), + ] { + assert_eq!(alert_name(alert), name, "{alert:?}"); + } + } + + /// Every alert rustls can decode has a registry name, so a numeric tag + /// only ever means a codepoint the registry does not assign. + #[test] + fn every_alert_rustls_decodes_has_a_registry_name() { + for code in 0..=u8::MAX { + let alert = AlertDescription::from(code); + if alert.as_str().is_some() { + assert!( + alert_name(alert).parse::().is_err(), + "{alert:?} ({code}) has no registry name" + ); + } + } + } + + /// Errors that are not a received alert keep the tag at `none`. + #[test] + fn non_alert_errors_carry_no_alert() { + for error in [ + io::Error::other("reset"), + io::Error::new(io::ErrorKind::UnexpectedEof, "eof"), + io::Error::new( + io::ErrorKind::InvalidData, + rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer), + ), + ] { + let failure = TlsFailure::io(&error); + assert_eq!(failure.alert, None); + assert_eq!(failure.alert_tag(), "none"); + } + } +} From 05d4b7a5c602ddf8c9de7ec0cb06eb656692df34 Mon Sep 17 00:00:00 2001 From: nikhilbirmiwal-pplx Date: Tue, 20 Oct 2026 14:08:53 +0000 Subject: [PATCH 2/2] net-gateway: tag the alert's wire value, drop the added tests Report the received alert as its numeric wire value (RFC 8446 section 6) on the metric tag and log field instead of a registry name. --- crates/libs/metrics/src/gateway.rs | 6 +- .../net/gateway/server/src/tcp_proxy/tls.rs | 65 +------ .../gateway/server/src/tcp_proxy/tls/tests.rs | 159 ------------------ 3 files changed, 12 insertions(+), 218 deletions(-) diff --git a/crates/libs/metrics/src/gateway.rs b/crates/libs/metrics/src/gateway.rs index 561835c7f..69aa82049 100644 --- a/crates/libs/metrics/src/gateway.rs +++ b/crates/libs/metrics/src/gateway.rs @@ -698,9 +698,9 @@ pub fn certificate_cache_entries(entries: usize, capacity: usize) { } /// One completed TLS handshake operation. `leg` names the two peers directly. -/// `alert` is the fatal alert the peer on that leg sent, named as in the IANA -/// TLS Alerts registry (`unknown_ca`, `bad_certificate`, ...), or `none` when -/// it sent none. On `client_to_netgw`, a certificate alert means the guest +/// `alert` is the wire value of the fatal alert the peer on that leg sent +/// (RFC 8446 section 6; `48` is `unknown_ca`, `42` is `bad_certificate`), or +/// `none` when it sent none. On `client_to_netgw`, a certificate alert means the guest /// rejected the interception chain; a disconnect or reset carries no alert. pub fn tls_handshake(leg: &str, outcome: &str, reason: &str, alert: &str) { counter( diff --git a/crates/libs/net/gateway/server/src/tcp_proxy/tls.rs b/crates/libs/net/gateway/server/src/tcp_proxy/tls.rs index 48f65ddcc..66a91fb63 100644 --- a/crates/libs/net/gateway/server/src/tcp_proxy/tls.rs +++ b/crates/libs/net/gateway/server/src/tcp_proxy/tls.rs @@ -64,7 +64,7 @@ impl TlsTransportError { outcome = self.as_str(), reason = failure.reason, io_kind = ?io_kind, - tls_alert = failure.alert_field().as_deref(), + tls_alert = failure.alert_code(), certificate_validation_reason = failure.certificate, "inspected connection failed" ); @@ -137,14 +137,15 @@ impl TlsFailure { } } - /// The `alert` metric tag and log field value. `none` when the peer sent - /// no alert. + /// The `alert` metric tag: the alert's wire value, or `none` when the + /// peer sent no alert. fn alert_tag(&self) -> Cow<'static, str> { - self.alert.map_or(Cow::Borrowed("none"), alert_name) + self.alert_code() + .map_or(Cow::Borrowed("none"), |code| Cow::Owned(code.to_string())) } - fn alert_field(&self) -> Option> { - self.alert.map(alert_name) + fn alert_code(&self) -> Option { + self.alert.map(u8::from) } } @@ -158,54 +159,6 @@ fn received_alert(error: &io::Error) -> Option { } } -/// The alert's description as named in the IANA TLS Alerts registry -/// (RFC 8446 section 6 and its updates), keyed by the wire value so the name -/// does not depend on how rustls spells its variants. A value the registry -/// does not assign is reported as its decimal wire value; the tag stays -/// bounded by the one-byte field. -fn alert_name(alert: rustls::AlertDescription) -> Cow<'static, str> { - let name = match u8::from(alert) { - 0 => "close_notify", - 10 => "unexpected_message", - 20 => "bad_record_mac", - 21 => "decryption_failed_RESERVED", - 22 => "record_overflow", - 30 => "decompression_failure_RESERVED", - 40 => "handshake_failure", - 41 => "no_certificate_RESERVED", - 42 => "bad_certificate", - 43 => "unsupported_certificate", - 44 => "certificate_revoked", - 45 => "certificate_expired", - 46 => "certificate_unknown", - 47 => "illegal_parameter", - 48 => "unknown_ca", - 49 => "access_denied", - 50 => "decode_error", - 51 => "decrypt_error", - 52 => "too_many_cids_requested", - 60 => "export_restriction_RESERVED", - 70 => "protocol_version", - 71 => "insufficient_security", - 80 => "internal_error", - 86 => "inappropriate_fallback", - 90 => "user_canceled", - 100 => "no_renegotiation_RESERVED", - 109 => "missing_extension", - 110 => "unsupported_extension", - 111 => "certificate_unobtainable_RESERVED", - 112 => "unrecognized_name", - 113 => "bad_certificate_status_response", - 114 => "bad_certificate_hash_value_RESERVED", - 115 => "unknown_psk_identity", - 116 => "certificate_required", - 120 => "no_application_protocol", - 121 => "ech_required", - unassigned => return Cow::Owned(unassigned.to_string()), - }; - Cow::Borrowed(name) -} - fn negotiated(alpn: Option<&[u8]>) -> Result { match selected_alpn(alpn)? { // TLS clients predating ALPN still use HTTP/1.1. Hyper parses their @@ -520,7 +473,7 @@ impl HandshakeAttempt { outcome, reason = failure.reason, io_kind = ?error.io_error().map(io::Error::kind), - tls_alert = failure.alert_field().as_deref(), + tls_alert = failure.alert_code(), certificate_validation_reason = failure.certificate, "TLS handshake failed" ); @@ -574,7 +527,7 @@ impl TlsIo { operation, reason = failure.reason, io_kind = ?error.kind(), - tls_alert = failure.alert_field().as_deref(), + tls_alert = failure.alert_code(), certificate_validation_reason = failure.certificate, "established TLS connection failed" ); diff --git a/crates/libs/net/gateway/server/src/tcp_proxy/tls/tests.rs b/crates/libs/net/gateway/server/src/tcp_proxy/tls/tests.rs index 3790d6bb7..ea09ac05c 100644 --- a/crates/libs/net/gateway/server/src/tcp_proxy/tls/tests.rs +++ b/crates/libs/net/gateway/server/src/tcp_proxy/tls/tests.rs @@ -253,162 +253,3 @@ mod http_failure { assert_eq!(http_reason(&error), "parse"); } } - -mod peer_alert { - use std::io; - use std::sync::Arc; - - use rustls::AlertDescription; - use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, ServerName}; - use tokio::io::{AsyncWriteExt, DuplexStream}; - use tokio_rustls::{TlsAcceptor, TlsConnector}; - - use super::super::{HandshakeAttempt, TlsFailure, TlsTransportError, alert_name}; - use crate::tls_interception::TlsHandshakeLeg; - - const HOST: &str = "origin.example"; - - fn provider() -> Arc { - Arc::new(rustls::crypto::aws_lc_rs::default_provider()) - } - - fn acceptor() -> TlsAcceptor { - let certified = rcgen::generate_simple_self_signed(vec![HOST.to_owned()]).unwrap(); - let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from( - certified.signing_key.serialize_der(), - )); - let config = rustls::ServerConfig::builder_with_provider(provider()) - .with_safe_default_protocol_versions() - .unwrap() - .with_no_client_auth() - .with_single_cert(vec![certified.cert.der().clone()], key) - .unwrap(); - TlsAcceptor::from(Arc::new(config)) - } - - /// A client whose trust store does not contain the server's issuer, as a - /// guest runtime with its own CA bundle sees the interception chain. - fn untrusting_connector() -> TlsConnector { - let config = rustls::ClientConfig::builder_with_provider(provider()) - .with_safe_default_protocol_versions() - .unwrap() - .with_root_certificates(rustls::RootCertStore::empty()) - .with_no_client_auth(); - TlsConnector::from(Arc::new(config)) - } - - async fn accept_error(client: impl AsyncFnOnce(DuplexStream)) -> io::Error { - let (server, client_stream) = tokio::io::duplex(64 * 1024); - let (accepted, ()) = tokio::join!(acceptor().accept(server), client(client_stream)); - let Err(error) = accepted else { - panic!("handshake must fail"); - }; - error - } - - fn handshake_tags(error: &TlsTransportError) -> Vec { - let capture = pplx_space_metrics::test_capture::capture_counters_guard(); - HandshakeAttempt::record_failure(TlsHandshakeLeg::ClientToNetgw, error); - let metrics = capture.finish(); - let handshake = metrics - .iter() - .find(|metric| metric.name == "gateway.tls.handshake") - .expect("handshake counter"); - handshake.tags.clone() - } - - /// The case interception rollout must see: the guest refuses the chain and - /// says why. The gateway records the guest's alert under its registry name. - #[tokio::test] - async fn guest_rejecting_the_chain_is_tagged_with_its_alert() { - let error = accept_error(async |stream| { - let name = ServerName::try_from(HOST).unwrap(); - let Err(_) = untrusting_connector().connect(name, stream).await else { - panic!("guest must refuse the chain"); - }; - }) - .await; - let error = TlsTransportError::Downstream(error); - - assert_eq!(error.failure().alert, Some(AlertDescription::UnknownCA)); - let tags = handshake_tags(&error); - assert!(tags.contains(&"alert:unknown_ca".to_owned()), "{tags:?}"); - assert!(tags.contains(&"reason:io".to_owned()), "{tags:?}"); - } - - /// A guest that goes away mid-handshake sent no alert, and must not be - /// counted as rejecting the chain. - #[tokio::test] - async fn guest_disconnecting_is_tagged_without_an_alert() { - let error = accept_error(async |mut stream| { - stream.shutdown().await.unwrap(); - }) - .await; - let error = TlsTransportError::Downstream(error); - - assert_eq!(error.failure().alert, None); - assert!(handshake_tags(&error).contains(&"alert:none".to_owned())); - } - - #[test] - fn alert_names_follow_the_iana_registry() { - for (alert, name) in [ - (AlertDescription::CloseNotify, "close_notify"), - (AlertDescription::HandshakeFailure, "handshake_failure"), - (AlertDescription::BadCertificate, "bad_certificate"), - ( - AlertDescription::UnsupportedCertificate, - "unsupported_certificate", - ), - (AlertDescription::CertificateExpired, "certificate_expired"), - (AlertDescription::CertificateUnknown, "certificate_unknown"), - (AlertDescription::UnknownCA, "unknown_ca"), - (AlertDescription::DecryptError, "decrypt_error"), - (AlertDescription::ProtocolVersion, "protocol_version"), - (AlertDescription::UnrecognisedName, "unrecognized_name"), - ( - AlertDescription::NoApplicationProtocol, - "no_application_protocol", - ), - ( - AlertDescription::EncryptedClientHelloRequired, - "ech_required", - ), - (AlertDescription::Unknown(255), "255"), - ] { - assert_eq!(alert_name(alert), name, "{alert:?}"); - } - } - - /// Every alert rustls can decode has a registry name, so a numeric tag - /// only ever means a codepoint the registry does not assign. - #[test] - fn every_alert_rustls_decodes_has_a_registry_name() { - for code in 0..=u8::MAX { - let alert = AlertDescription::from(code); - if alert.as_str().is_some() { - assert!( - alert_name(alert).parse::().is_err(), - "{alert:?} ({code}) has no registry name" - ); - } - } - } - - /// Errors that are not a received alert keep the tag at `none`. - #[test] - fn non_alert_errors_carry_no_alert() { - for error in [ - io::Error::other("reset"), - io::Error::new(io::ErrorKind::UnexpectedEof, "eof"), - io::Error::new( - io::ErrorKind::InvalidData, - rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer), - ), - ] { - let failure = TlsFailure::io(&error); - assert_eq!(failure.alert, None); - assert_eq!(failure.alert_tag(), "none"); - } - } -}