From 17bfb052c552257ee241e52797090378973f2fd5 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 2 Jul 2021 11:35:00 +0200 Subject: [PATCH] refactor(errorsx): start hiding private details and moving around stuff (#424) * refactor(errorsx): start hiding private details and moving around stuff Part of https://github.com/ooni/probe/issues/1505 * fix: remove now-addressed todo comments --- internal/errorsx/dialer.go | 10 +- internal/errorsx/errorsx.go | 210 ------------------------------- internal/errorsx/errorsx_test.go | 34 ++--- internal/errorsx/failures.go | 48 +++++++ internal/errorsx/operations.go | 44 +++++++ internal/errorsx/quic.go | 85 ++++++++++++- internal/errorsx/resolver.go | 15 ++- internal/errorsx/tls.go | 25 +++- 8 files changed, 235 insertions(+), 236 deletions(-) create mode 100644 internal/errorsx/failures.go create mode 100644 internal/errorsx/operations.go diff --git a/internal/errorsx/dialer.go b/internal/errorsx/dialer.go index d92e1a3..b439296 100644 --- a/internal/errorsx/dialer.go +++ b/internal/errorsx/dialer.go @@ -11,12 +11,12 @@ type Dialer interface { DialContext(ctx context.Context, network, address string) (net.Conn, error) } -// ErrorWrapperDialer is a dialer that performs err wrapping. +// ErrorWrapperDialer is a dialer that performs error wrapping. type ErrorWrapperDialer struct { Dialer } -// DialContext implements Dialer.DialContext +// DialContext implements Dialer.DialContext. func (d *ErrorWrapperDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { conn, err := d.Dialer.DialContext(ctx, network, address) err = SafeErrWrapperBuilder{ @@ -34,7 +34,7 @@ type errorWrapperConn struct { net.Conn } -// Read implements net.Conn.Read +// Read implements net.Conn.Read. func (c *errorWrapperConn) Read(b []byte) (n int, err error) { n, err = c.Conn.Read(b) err = SafeErrWrapperBuilder{ @@ -44,7 +44,7 @@ func (c *errorWrapperConn) Read(b []byte) (n int, err error) { return } -// Write implements net.Conn.Write +// Write implements net.Conn.Write. func (c *errorWrapperConn) Write(b []byte) (n int, err error) { n, err = c.Conn.Write(b) err = SafeErrWrapperBuilder{ @@ -54,7 +54,7 @@ func (c *errorWrapperConn) Write(b []byte) (n int, err error) { return } -// Close implements net.Conn.Close +// Close implements net.Conn.Close. func (c *errorWrapperConn) Close() (err error) { err = c.Conn.Close() err = SafeErrWrapperBuilder{ diff --git a/internal/errorsx/errorsx.go b/internal/errorsx/errorsx.go index df8d383..4a4b78f 100644 --- a/internal/errorsx/errorsx.go +++ b/internal/errorsx/errorsx.go @@ -1,142 +1,16 @@ // Package errorsx contains error extensions. package errorsx -// TODO: eventually we want to re-structure the error classification code by clearly separating the layers where the error occur: -// -// - errno.go and errno_test.go: contain only the errno classifier (for system errors) -// - qtls.go and qtls_test.go: contain qtls dialers, handshaker, classifier -// - tls.go and tls_test.go: contain tls dialers, handshaker, classifier -// - resolver.go and resolver_test.go: contain dialers and classifier for resolving - import ( "context" - "crypto/x509" "errors" "fmt" "strings" "syscall" - "github.com/lucas-clemente/quic-go" "github.com/ooni/probe-cli/v3/internal/scrubber" ) -const ( - // FailureConnectionRefused means ECONNREFUSED. - FailureConnectionRefused = "connection_refused" - - // FailureConnectionReset means ECONNRESET. - FailureConnectionReset = "connection_reset" - - // FailureDNSBogonError means we detected bogon in DNS reply. - FailureDNSBogonError = "dns_bogon_error" - - // FailureDNSNXDOMAINError means we got NXDOMAIN in DNS reply. - FailureDNSNXDOMAINError = "dns_nxdomain_error" - - // FailureEOFError means we got unexpected EOF on connection. - FailureEOFError = "eof_error" - - // FailureGenericTimeoutError means we got some timer has expired. - FailureGenericTimeoutError = "generic_timeout_error" - - // FailureHostUnreachable means that there is "no route to host". - FailureHostUnreachable = "host_unreachable" - - // FailureInterrupted means that the user interrupted us. - FailureInterrupted = "interrupted" - - // FailureNoCompatibleQUICVersion means that the server does not support the proposed QUIC version - FailureNoCompatibleQUICVersion = "quic_incompatible_version" - - // FailureSSLHandshake means that the negotiation of cryptographic parameters failed - FailureSSLHandshake = "ssl_failed_handshake" - - // FailureSSLInvalidHostname means we got certificate is not valid for SNI. - FailureSSLInvalidHostname = "ssl_invalid_hostname" - - // FailureSSLUnknownAuthority means we cannot find CA validating certificate. - FailureSSLUnknownAuthority = "ssl_unknown_authority" - - // FailureSSLInvalidCertificate means certificate experired or other - // sort of errors causing it to be invalid. - FailureSSLInvalidCertificate = "ssl_invalid_certificate" - - // FailureJSONParseError indicates that we couldn't parse a JSON - FailureJSONParseError = "json_parse_error" -) - -// TLS alert protocol as defined in RFC8446 -const ( - // Sender was unable to negotiate an acceptable set of security parameters given the options available. - TLSAlertHandshakeFailure = 40 - - // Certificate was corrupt, contained signatures that did not verify correctly, etc. - TLSAlertBadCertificate = 42 - - // Certificate was of an unsupported type. - TLSAlertUnsupportedCertificate = 43 - - // Certificate was revoked by its signer. - TLSAlertCertificateRevoked = 44 - - // Certificate has expired or is not currently valid. - TLSAlertCertificateExpired = 45 - - // Some unspecified issue arose in processing the certificate, rendering it unacceptable. - TLSAlertCertificateUnknown = 46 - - // Certificate was not accepted because the CA certificate could not be located or could not be matched with a known trust anchor. - TLSAlertUnknownCA = 48 - - // Handshake (not record layer) cryptographic operation failed. - TLSAlertDecryptError = 51 - - // Sent by servers when no server exists identified by the name provided by the client via the "server_name" extension. - TLSUnrecognizedName = 112 -) - -const ( - // ResolveOperation is the operation where we resolve a domain name - ResolveOperation = "resolve" - - // ConnectOperation is the operation where we do a TCP connect - ConnectOperation = "connect" - - // TLSHandshakeOperation is the TLS handshake - TLSHandshakeOperation = "tls_handshake" - - // QUICHandshakeOperation is the handshake to setup a QUIC connection - QUICHandshakeOperation = "quic_handshake" - - // QUICListenOperation is when we open a listening UDP conn for QUIC - QUICListenOperation = "quic_listen" - - // HTTPRoundTripOperation is the HTTP round trip - HTTPRoundTripOperation = "http_round_trip" - - // CloseOperation is when we close a socket - CloseOperation = "close" - - // ReadOperation is when we read from a socket - ReadOperation = "read" - - // WriteOperation is when we write to a socket - WriteOperation = "write" - - // ReadFromOperation is when we read from an UDP socket - ReadFromOperation = "read_from" - - // WriteToOperation is when we write to an UDP socket - WriteToOperation = "write_to" - - // UnknownOperation is when we cannot determine the operation - UnknownOperation = "unknown" - - // TopLevelOperation is used when the failure happens at top level. This - // happens for example with urlgetter with a cancelled context. - TopLevelOperation = "top_level" -) - // ErrDNSBogon indicates that we found a bogon address. This is the // correct value with which to initialize MeasurementRoot.ErrDNSBogon // to tell this library to return an error when a bogon is found. @@ -287,82 +161,6 @@ func toFailureString(err error) string { return scrubber.Scrub(formatted) // scrub IP addresses in the error } -// ClassifyQUICFailure is a classifier to translate QUIC errors to OONI error strings. -// TODO(kelmenhorst,bassosimone): Consider moving this into quicdialer. -func ClassifyQUICFailure(err error) string { - var versionNegotiation *quic.VersionNegotiationError - var statelessReset *quic.StatelessResetError - var handshakeTimeout *quic.HandshakeTimeoutError - var idleTimeout *quic.IdleTimeoutError - var transportError *quic.TransportError - - if errors.As(err, &versionNegotiation) { - return FailureNoCompatibleQUICVersion - } - if errors.As(err, &statelessReset) { - return FailureConnectionReset - } - if errors.As(err, &handshakeTimeout) { - return FailureGenericTimeoutError - } - if errors.As(err, &idleTimeout) { - return FailureGenericTimeoutError - } - if errors.As(err, &transportError) { - if transportError.ErrorCode == quic.ConnectionRefused { - return FailureConnectionRefused - } - // the TLS Alert constants are taken from RFC8446 - errCode := uint8(transportError.ErrorCode) - if isCertificateError(errCode) { - return FailureSSLInvalidCertificate - } - // TLSAlertDecryptError and TLSAlertHandshakeFailure are summarized to a FailureSSLHandshake error because both - // alerts are caused by a failed or corrupted parameter negotiation during the TLS handshake. - if errCode == TLSAlertDecryptError || errCode == TLSAlertHandshakeFailure { - return FailureSSLHandshake - } - if errCode == TLSAlertUnknownCA { - return FailureSSLUnknownAuthority - } - if errCode == TLSUnrecognizedName { - return FailureSSLInvalidHostname - } - } - return toFailureString(err) -} - -// ClassifyResolveFailure is a classifier to translate DNS resolving errors to OONI error strings. -// TODO(kelmenhorst,bassosimone): Consider moving this into resolve. -func ClassifyResolveFailure(err error) string { - if errors.Is(err, ErrDNSBogon) { - return FailureDNSBogonError // not in MK - } - return toFailureString(err) -} - -// ClassifyTLSFailure is a classifier to translate TLS errors to OONI error strings. -// TODO(kelmenhorst,bassosimone): Consider moving this into tlsdialer. -func ClassifyTLSFailure(err error) string { - var x509HostnameError x509.HostnameError - if errors.As(err, &x509HostnameError) { - // Test case: https://wrong.host.badssl.com/ - return FailureSSLInvalidHostname - } - var x509UnknownAuthorityError x509.UnknownAuthorityError - if errors.As(err, &x509UnknownAuthorityError) { - // Test case: https://self-signed.badssl.com/. This error has - // never been among the ones returned by MK. - return FailureSSLUnknownAuthority - } - var x509CertificateInvalidError x509.CertificateInvalidError - if errors.As(err, &x509CertificateInvalidError) { - // Test case: https://expired.badssl.com/ - return FailureSSLInvalidCertificate - } - return toFailureString(err) -} - func toOperationString(err error, operation string) string { var errwrapper *ErrWrapper if errors.As(err, &errwrapper) { @@ -393,11 +191,3 @@ func toOperationString(err error, operation string) string { } return operation } - -func isCertificateError(alert uint8) bool { - return (alert == TLSAlertBadCertificate || - alert == TLSAlertUnsupportedCertificate || - alert == TLSAlertCertificateExpired || - alert == TLSAlertCertificateRevoked || - alert == TLSAlertCertificateUnknown) -} diff --git a/internal/errorsx/errorsx_test.go b/internal/errorsx/errorsx_test.go index 252784f..6d34825 100644 --- a/internal/errorsx/errorsx_test.go +++ b/internal/errorsx/errorsx_test.go @@ -157,51 +157,51 @@ func TestToFailureString(t *testing.T) { func TestClassifyQUICFailure(t *testing.T) { t.Run("for connection_reset", func(t *testing.T) { - if ClassifyQUICFailure(&quic.StatelessResetError{}) != FailureConnectionReset { + if classifyQUICFailure(&quic.StatelessResetError{}) != FailureConnectionReset { t.Fatal("unexpected results") } }) t.Run("for incompatible quic version", func(t *testing.T) { - if ClassifyQUICFailure(&quic.VersionNegotiationError{}) != FailureNoCompatibleQUICVersion { + if classifyQUICFailure(&quic.VersionNegotiationError{}) != FailureNoCompatibleQUICVersion { t.Fatal("unexpected results") } }) t.Run("for quic connection refused", func(t *testing.T) { - if ClassifyQUICFailure(&quic.TransportError{ErrorCode: quic.ConnectionRefused}) != FailureConnectionRefused { + if classifyQUICFailure(&quic.TransportError{ErrorCode: quic.ConnectionRefused}) != FailureConnectionRefused { t.Fatal("unexpected results") } }) t.Run("for quic handshake timeout", func(t *testing.T) { - if ClassifyQUICFailure(&quic.HandshakeTimeoutError{}) != FailureGenericTimeoutError { + if classifyQUICFailure(&quic.HandshakeTimeoutError{}) != FailureGenericTimeoutError { t.Fatal("unexpected results") } }) t.Run("for QUIC idle connection timeout", func(t *testing.T) { - if ClassifyQUICFailure(&quic.IdleTimeoutError{}) != FailureGenericTimeoutError { + if classifyQUICFailure(&quic.IdleTimeoutError{}) != FailureGenericTimeoutError { t.Fatal("unexpected results") } }) t.Run("for QUIC CRYPTO Handshake", func(t *testing.T) { - var err quic.TransportErrorCode = TLSAlertHandshakeFailure - if ClassifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLHandshake { + var err quic.TransportErrorCode = quicTLSAlertHandshakeFailure + if classifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLHandshake { t.Fatal("unexpected results") } }) t.Run("for QUIC CRYPTO Invalid Certificate", func(t *testing.T) { - var err quic.TransportErrorCode = TLSAlertBadCertificate - if ClassifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidCertificate { + var err quic.TransportErrorCode = quicTLSAlertBadCertificate + if classifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidCertificate { t.Fatal("unexpected results") } }) t.Run("for QUIC CRYPTO Unknown CA", func(t *testing.T) { - var err quic.TransportErrorCode = TLSAlertUnknownCA - if ClassifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLUnknownAuthority { + var err quic.TransportErrorCode = quicTLSAlertUnknownCA + if classifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLUnknownAuthority { t.Fatal("unexpected results") } }) t.Run("for QUIC CRYPTO Bad Hostname", func(t *testing.T) { - var err quic.TransportErrorCode = TLSUnrecognizedName - if ClassifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidHostname { + var err quic.TransportErrorCode = quicTLSUnrecognizedName + if classifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidHostname { t.Fatal("unexpected results") } }) @@ -210,7 +210,7 @@ func TestClassifyQUICFailure(t *testing.T) { func TestClassifyResolveFailure(t *testing.T) { t.Run("for ErrDNSBogon", func(t *testing.T) { - if ClassifyResolveFailure(ErrDNSBogon) != FailureDNSBogonError { + if classifyResolveFailure(ErrDNSBogon) != FailureDNSBogonError { t.Fatal("unexpected result") } }) @@ -219,19 +219,19 @@ func TestClassifyResolveFailure(t *testing.T) { func TestClassifyTLSFailure(t *testing.T) { t.Run("for x509.HostnameError", func(t *testing.T) { var err x509.HostnameError - if ClassifyTLSFailure(err) != FailureSSLInvalidHostname { + if classifyTLSFailure(err) != FailureSSLInvalidHostname { t.Fatal("unexpected result") } }) t.Run("for x509.UnknownAuthorityError", func(t *testing.T) { var err x509.UnknownAuthorityError - if ClassifyTLSFailure(err) != FailureSSLUnknownAuthority { + if classifyTLSFailure(err) != FailureSSLUnknownAuthority { t.Fatal("unexpected result") } }) t.Run("for x509.CertificateInvalidError", func(t *testing.T) { var err x509.CertificateInvalidError - if ClassifyTLSFailure(err) != FailureSSLInvalidCertificate { + if classifyTLSFailure(err) != FailureSSLInvalidCertificate { t.Fatal("unexpected result") } }) diff --git a/internal/errorsx/failures.go b/internal/errorsx/failures.go new file mode 100644 index 0000000..70b5109 --- /dev/null +++ b/internal/errorsx/failures.go @@ -0,0 +1,48 @@ +package errorsx + +// This enumeration lists all the failures defined at +// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md +const ( + // FailureConnectionRefused means ECONNREFUSED. + FailureConnectionRefused = "connection_refused" + + // FailureConnectionReset means ECONNRESET. + FailureConnectionReset = "connection_reset" + + // FailureDNSBogonError means we detected bogon in DNS reply. + FailureDNSBogonError = "dns_bogon_error" + + // FailureDNSNXDOMAINError means we got NXDOMAIN in DNS reply. + FailureDNSNXDOMAINError = "dns_nxdomain_error" + + // FailureEOFError means we got unexpected EOF on connection. + FailureEOFError = "eof_error" + + // FailureGenericTimeoutError means we got some timer has expired. + FailureGenericTimeoutError = "generic_timeout_error" + + // FailureHostUnreachable means that there is "no route to host". + FailureHostUnreachable = "host_unreachable" + + // FailureInterrupted means that the user interrupted us. + FailureInterrupted = "interrupted" + + // FailureNoCompatibleQUICVersion means that the server does not support the proposed QUIC version + FailureNoCompatibleQUICVersion = "quic_incompatible_version" + + // FailureSSLHandshake means that the negotiation of cryptographic parameters failed + FailureSSLHandshake = "ssl_failed_handshake" + + // FailureSSLInvalidHostname means we got certificate is not valid for SNI. + FailureSSLInvalidHostname = "ssl_invalid_hostname" + + // FailureSSLUnknownAuthority means we cannot find CA validating certificate. + FailureSSLUnknownAuthority = "ssl_unknown_authority" + + // FailureSSLInvalidCertificate means certificate experired or other + // sort of errors causing it to be invalid. + FailureSSLInvalidCertificate = "ssl_invalid_certificate" + + // FailureJSONParseError indicates that we couldn't parse a JSON + FailureJSONParseError = "json_parse_error" +) diff --git a/internal/errorsx/operations.go b/internal/errorsx/operations.go new file mode 100644 index 0000000..e0f9d1c --- /dev/null +++ b/internal/errorsx/operations.go @@ -0,0 +1,44 @@ +package errorsx + +// Operations that we measure. +const ( + // ResolveOperation is the operation where we resolve a domain name. + ResolveOperation = "resolve" + + // ConnectOperation is the operation where we do a TCP connect. + ConnectOperation = "connect" + + // TLSHandshakeOperation is the TLS handshake. + TLSHandshakeOperation = "tls_handshake" + + // QUICHandshakeOperation is the handshake to setup a QUIC connection. + QUICHandshakeOperation = "quic_handshake" + + // QUICListenOperation is when we open a listening UDP conn for QUIC. + QUICListenOperation = "quic_listen" + + // HTTPRoundTripOperation is the HTTP round trip. + HTTPRoundTripOperation = "http_round_trip" + + // CloseOperation is when we close a socket. + CloseOperation = "close" + + // ReadOperation is when we read from a socket. + ReadOperation = "read" + + // WriteOperation is when we write to a socket. + WriteOperation = "write" + + // ReadFromOperation is when we read from an UDP socket. + ReadFromOperation = "read_from" + + // WriteToOperation is when we write to an UDP socket. + WriteToOperation = "write_to" + + // UnknownOperation is when we cannot determine the operation. + UnknownOperation = "unknown" + + // TopLevelOperation is used when the failure happens at top level. This + // happens for example with urlgetter with a cancelled context. + TopLevelOperation = "top_level" +) diff --git a/internal/errorsx/quic.go b/internal/errorsx/quic.go index 72e51ef..a997769 100644 --- a/internal/errorsx/quic.go +++ b/internal/errorsx/quic.go @@ -3,6 +3,7 @@ package errorsx import ( "context" "crypto/tls" + "errors" "net" "github.com/lucas-clemente/quic-go" @@ -86,7 +87,7 @@ func (d *ErrorWrapperQUICDialer) DialContext( tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) { sess, err := d.Dialer.DialContext(ctx, network, host, tlsCfg, cfg) err = SafeErrWrapperBuilder{ - Classifier: ClassifyQUICFailure, + Classifier: classifyQUICFailure, Error: err, Operation: QUICHandshakeOperation, }.MaybeBuild() @@ -95,3 +96,85 @@ func (d *ErrorWrapperQUICDialer) DialContext( } return sess, nil } + +// classifyQUICFailure is a classifier to translate QUIC errors to OONI error strings. +func classifyQUICFailure(err error) string { + var versionNegotiation *quic.VersionNegotiationError + var statelessReset *quic.StatelessResetError + var handshakeTimeout *quic.HandshakeTimeoutError + var idleTimeout *quic.IdleTimeoutError + var transportError *quic.TransportError + + if errors.As(err, &versionNegotiation) { + return FailureNoCompatibleQUICVersion + } + if errors.As(err, &statelessReset) { + return FailureConnectionReset + } + if errors.As(err, &handshakeTimeout) { + return FailureGenericTimeoutError + } + if errors.As(err, &idleTimeout) { + return FailureGenericTimeoutError + } + if errors.As(err, &transportError) { + if transportError.ErrorCode == quic.ConnectionRefused { + return FailureConnectionRefused + } + // the TLS Alert constants are taken from RFC8446 + errCode := uint8(transportError.ErrorCode) + if quicIsCertificateError(errCode) { + return FailureSSLInvalidCertificate + } + // TLSAlertDecryptError and TLSAlertHandshakeFailure are summarized to a FailureSSLHandshake error because both + // alerts are caused by a failed or corrupted parameter negotiation during the TLS handshake. + if errCode == quicTLSAlertDecryptError || errCode == quicTLSAlertHandshakeFailure { + return FailureSSLHandshake + } + if errCode == quicTLSAlertUnknownCA { + return FailureSSLUnknownAuthority + } + if errCode == quicTLSUnrecognizedName { + return FailureSSLInvalidHostname + } + } + return toFailureString(err) +} + +// TLS alert protocol as defined in RFC8446 +const ( + // Sender was unable to negotiate an acceptable set of security parameters given the options available. + quicTLSAlertHandshakeFailure = 40 + + // Certificate was corrupt, contained signatures that did not verify correctly, etc. + quicTLSAlertBadCertificate = 42 + + // Certificate was of an unsupported type. + quicTLSAlertUnsupportedCertificate = 43 + + // Certificate was revoked by its signer. + quicTLSAlertCertificateRevoked = 44 + + // Certificate has expired or is not currently valid. + quicTLSAlertCertificateExpired = 45 + + // Some unspecified issue arose in processing the certificate, rendering it unacceptable. + quicTLSAlertCertificateUnknown = 46 + + // Certificate was not accepted because the CA certificate could not be located or could not be matched with a known trust anchor. + quicTLSAlertUnknownCA = 48 + + // Handshake (not record layer) cryptographic operation failed. + quicTLSAlertDecryptError = 51 + + // Sent by servers when no server exists identified by the name provided by the client via the "server_name" extension. + quicTLSUnrecognizedName = 112 +) + +func quicIsCertificateError(alert uint8) bool { + return (alert == quicTLSAlertBadCertificate || + alert == quicTLSAlertUnsupportedCertificate || + alert == quicTLSAlertCertificateExpired || + alert == quicTLSAlertCertificateRevoked || + alert == quicTLSAlertCertificateUnknown) +} diff --git a/internal/errorsx/resolver.go b/internal/errorsx/resolver.go index 459ce83..b5c58f6 100644 --- a/internal/errorsx/resolver.go +++ b/internal/errorsx/resolver.go @@ -1,6 +1,9 @@ package errorsx -import "context" +import ( + "context" + "errors" +) // Resolver is a DNS resolver. The *net.Resolver used by Go implements // this interface, but other implementations are possible. @@ -20,13 +23,21 @@ var _ Resolver = &ErrorWrapperResolver{} func (r *ErrorWrapperResolver) LookupHost(ctx context.Context, hostname string) ([]string, error) { addrs, err := r.Resolver.LookupHost(ctx, hostname) err = SafeErrWrapperBuilder{ - Classifier: ClassifyResolveFailure, + Classifier: classifyResolveFailure, Error: err, Operation: ResolveOperation, }.MaybeBuild() return addrs, err } +// classifyResolveFailure is a classifier to translate DNS resolving errors to OONI error strings. +func classifyResolveFailure(err error) string { + if errors.Is(err, ErrDNSBogon) { + return FailureDNSBogonError // not in MK + } + return toFailureString(err) +} + type resolverNetworker interface { Network() string } diff --git a/internal/errorsx/tls.go b/internal/errorsx/tls.go index 46fbdf5..f408327 100644 --- a/internal/errorsx/tls.go +++ b/internal/errorsx/tls.go @@ -3,6 +3,8 @@ package errorsx import ( "context" "crypto/tls" + "crypto/x509" + "errors" "net" ) @@ -23,9 +25,30 @@ func (h *ErrorWrapperTLSHandshaker) Handshake( ) (net.Conn, tls.ConnectionState, error) { tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config) err = SafeErrWrapperBuilder{ - Classifier: ClassifyTLSFailure, + Classifier: classifyTLSFailure, Error: err, Operation: TLSHandshakeOperation, }.MaybeBuild() return tlsconn, state, err } + +// classifyTLSFailure is a classifier to translate TLS errors to OONI error strings. +func classifyTLSFailure(err error) string { + var x509HostnameError x509.HostnameError + if errors.As(err, &x509HostnameError) { + // Test case: https://wrong.host.badssl.com/ + return FailureSSLInvalidHostname + } + var x509UnknownAuthorityError x509.UnknownAuthorityError + if errors.As(err, &x509UnknownAuthorityError) { + // Test case: https://self-signed.badssl.com/. This error has + // never been among the ones returned by MK. + return FailureSSLUnknownAuthority + } + var x509CertificateInvalidError x509.CertificateInvalidError + if errors.As(err, &x509CertificateInvalidError) { + // Test case: https://expired.badssl.com/ + return FailureSSLInvalidCertificate + } + return toFailureString(err) +}