diff --git a/internal/engine/netx/errorx/errno_unix.go b/internal/engine/netx/errorx/errno_unix.go new file mode 100644 index 0000000..57491b7 --- /dev/null +++ b/internal/engine/netx/errorx/errno_unix.go @@ -0,0 +1,34 @@ +package errorx + +import "golang.org/x/sys/unix" + +const ( + ECANCELED = unix.ECANCELED + ECONNREFUSED = unix.ECONNREFUSED + ECONNRESET = unix.ECONNRESET + EHOSTUNREACH = unix.EHOSTUNREACH + ETIMEDOUT = unix.ETIMEDOUT + EAFNOSUPPORT = unix.EAFNOSUPPORT + EADDRINUSE = unix.EADDRINUSE + EADDRNOTAVAIL = unix.EADDRNOTAVAIL + EISCONN = unix.EISCONN + EFAULT = unix.EFAULT + EBADF = unix.EBADF + ECONNABORTED = unix.ECONNABORTED + EALREADY = unix.EALREADY + EDESTADDRREQ = unix.EDESTADDRREQ + EINTR = unix.EINTR + EINVAL = unix.EINVAL + EMSGSIZE = unix.EMSGSIZE + ENETDOWN = unix.ENETDOWN + ENETRESET = unix.ENETRESET + ENETUNREACH = unix.ENETUNREACH + ENOBUFS = unix.ENOBUFS + ENOPROTOOPT = unix.ENOPROTOOPT + ENOTSOCK = unix.ENOTSOCK + ENOTCONN = unix.ENOTCONN + EWOULDBLOCK = unix.EWOULDBLOCK + EACCES = unix.EACCES + EPROTONOSUPPORT = unix.EPROTONOSUPPORT + EPROTOTYPE = unix.EPROTOTYPE +) diff --git a/internal/engine/netx/errorx/errno_windows.go b/internal/engine/netx/errorx/errno_windows.go new file mode 100644 index 0000000..dbe6826 --- /dev/null +++ b/internal/engine/netx/errorx/errno_windows.go @@ -0,0 +1,34 @@ +package errorx + +import "golang.org/x/sys/windows" + +const ( + ECANCELED = windows.ECANCELED + ECONNREFUSED = windows.ECONNREFUSED + ECONNRESET = windows.ECONNRESET + EHOSTUNREACH = windows.EHOSTUNREACH + ETIMEDOUT = windows.ETIMEDOUT + EAFNOSUPPORT = windows.EAFNOSUPPORT + EADDRINUSE = windows.EADDRINUSE + EADDRNOTAVAIL = windows.EADDRNOTAVAIL + EISCONN = windows.EISCONN + EFAULT = windows.EFAULT + EBADF = windows.EBADF + ECONNABORTED = windows.ECONNABORTED + EALREADY = windows.EALREADY + EDESTADDRREQ = windows.EDESTADDRREQ + EINTR = windows.EINTR + EINVAL = windows.EINVAL + EMSGSIZE = windows.EMSGSIZE + ENETDOWN = windows.ENETDOWN + ENETRESET = windows.ENETRESET + ENETUNREACH = windows.ENETUNREACH + ENOBUFS = windows.ENOBUFS + ENOPROTOOPT = windows.ENOPROTOOPT + ENOTSOCK = windows.ENOTSOCK + ENOTCONN = windows.ENOTCONN + EWOULDBLOCK = windows.EWOULDBLOCK + EACCES = windows.EACCES + EPROTONOSUPPORT = windows.EPROTONOSUPPORT + EPROTOTYPE = windows.EPROTOTYPE +) diff --git a/internal/engine/netx/errorx/errorx.go b/internal/engine/netx/errorx/errorx.go index 1d968e9..b2ac052 100644 --- a/internal/engine/netx/errorx/errorx.go +++ b/internal/engine/netx/errorx/errorx.go @@ -1,14 +1,22 @@ // Package errorx contains error extensions package errorx +// 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" - "regexp" "strings" + "syscall" + "github.com/lucas-clemente/quic-go" "github.com/ooni/probe-cli/v3/internal/scrubber" ) @@ -31,12 +39,18 @@ const ( // 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" @@ -51,6 +65,36 @@ const ( 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" @@ -166,6 +210,10 @@ type SafeErrWrapperBuilder struct { // Error is the error, if any Error error + // Classifier is the local error to string classifier. When there is no + // configured classifier we will use the generic classifier. + Classifier func(err error) string + // Operation is the operation that failed Operation string @@ -177,10 +225,14 @@ type SafeErrWrapperBuilder struct { // a nil error value, instead, if b.Error is nil. func (b SafeErrWrapperBuilder) MaybeBuild() (err error) { if b.Error != nil { + classifier := b.Classifier + if classifier == nil { + classifier = toFailureString + } err = &ErrWrapper{ ConnID: b.ConnID, DialID: b.DialID, - Failure: toFailureString(b.Error), + Failure: classifier(b.Error), Operation: toOperationString(b.Error, b.Operation), TransactionID: b.TransactionID, WrappedErr: b.Error, @@ -189,6 +241,10 @@ func (b SafeErrWrapperBuilder) MaybeBuild() (err error) { return } +// TODO (kelmenhorst, bassosimone): +// Use errors.Is / errors.As more often, when possible, in this classifier. +// These methods are more robust to library changes than strings. +// errors.Is / errors.As can only be used when the error is exported. func toFailureString(err error) string { // The list returned here matches the values used by MK unless // explicitly noted otherwise with a comment. @@ -198,12 +254,114 @@ func toFailureString(err error) string { return errwrapper.Error() // we've already wrapped it } - if errors.Is(err, ErrDNSBogon) { - return FailureDNSBogonError // not in MK + // filter out system errors: necessary to detect all windows errors + // https://github.com/ooni/probe/issues/1526 describes the problem of mapping localized windows errors + var errno syscall.Errno + if errors.As(err, &errno) { + switch errno { + case ECANCELED: + return FailureInterrupted + case ECONNRESET: + return FailureConnectionReset + case ECONNREFUSED: + return FailureConnectionRefused + case EHOSTUNREACH: + return FailureHostUnreachable + case ETIMEDOUT: + return FailureGenericTimeoutError + // TODO(kelmenhorst): find out if we need more system errors here + } } if errors.Is(err, context.Canceled) { return FailureInterrupted } + s := err.Error() + if strings.HasSuffix(s, "operation was canceled") { + return FailureInterrupted + } + if strings.HasSuffix(s, "EOF") { + return FailureEOFError + } + if strings.HasSuffix(s, "context deadline exceeded") { + return FailureGenericTimeoutError + } + if strings.HasSuffix(s, "transaction is timed out") { + return FailureGenericTimeoutError + } + if strings.HasSuffix(s, "i/o timeout") { + return FailureGenericTimeoutError + } + // TODO(kelmenhorst,bassosimone): this can probably be moved since it's TLS specific + if strings.HasSuffix(s, "TLS handshake timeout") { + return FailureGenericTimeoutError + } + if strings.HasSuffix(s, "no such host") { + // This is dns_lookup_error in MK but such error is used as a + // generic "hey, the lookup failed" error. Instead, this error + // that we return here is significantly more specific. + return FailureDNSNXDOMAINError + } + formatted := fmt.Sprintf("unknown_failure: %s", s) + 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/ @@ -220,76 +378,7 @@ func toFailureString(err error) string { // Test case: https://expired.badssl.com/ return FailureSSLInvalidCertificate } - - s := err.Error() - if strings.HasSuffix(s, "operation was canceled") { - return FailureInterrupted - } - if strings.HasSuffix(s, "EOF") { - return FailureEOFError - } - if strings.HasSuffix(s, "connection refused") { - return FailureConnectionRefused - } - if strings.HasSuffix(s, "connection reset by peer") { - return FailureConnectionReset - } - if strings.HasSuffix(s, "context deadline exceeded") { - return FailureGenericTimeoutError - } - if strings.HasSuffix(s, "transaction is timed out") { - return FailureGenericTimeoutError - } - if strings.HasSuffix(s, "i/o timeout") { - return FailureGenericTimeoutError - } - if strings.HasSuffix(s, "TLS handshake timeout") { - return FailureGenericTimeoutError - } - if strings.HasSuffix(s, "no such host") { - // This is dns_lookup_error in MK but such error is used as a - // generic "hey, the lookup failed" error. Instead, this error - // that we return here is significantly more specific. - return FailureDNSNXDOMAINError - } - - // TODO(kelmenhorst): see whether it is possible to match errors - // from qtls rather than strings for TLS errors below. - // - // TODO(kelmenhorst): make sure we have tests for all errors. Also, - // how to ensure we are robust to changes in other libs? - // - // special QUIC errors - matched, err := regexp.MatchString(`.*x509: certificate is valid for.*not.*`, s) - if matched { - return FailureSSLInvalidHostname - } - if strings.HasSuffix(s, "x509: certificate signed by unknown authority") { - return FailureSSLUnknownAuthority - } - certInvalidErrors := []string{"x509: certificate is not authorized to sign other certificates", "x509: certificate has expired or is not yet valid:", "x509: a root or intermediate certificate is not authorized to sign for this name:", "x509: a root or intermediate certificate is not authorized for an extended key usage:", "x509: too many intermediates for path length constraint", "x509: certificate specifies an incompatible key usage", "x509: issuer name does not match subject from issuing certificate", "x509: issuer has name constraints but leaf doesn't have a SAN extension", "x509: issuer has name constraints but leaf contains unknown or unconstrained name:"} - for _, errstr := range certInvalidErrors { - if strings.Contains(s, errstr) { - return FailureSSLInvalidCertificate - } - } - if strings.HasPrefix(s, "No compatible QUIC version found") { - return FailureNoCompatibleQUICVersion - } - if strings.HasSuffix(s, "Handshake did not complete in time") { - return FailureGenericTimeoutError - } - if strings.HasSuffix(s, "connection_refused") { - return FailureConnectionRefused - } - if strings.Contains(s, "stateless_reset") { - return FailureConnectionReset - } - if strings.Contains(s, "deadline exceeded") { - return FailureGenericTimeoutError - } - formatted := fmt.Sprintf("unknown_failure: %s", s) - return scrubber.Scrub(formatted) // scrub IP addresses in the error + return toFailureString(err) } func toOperationString(err error, operation string) string { @@ -322,3 +411,11 @@ 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/engine/netx/errorx/errorx_test.go b/internal/engine/netx/errorx/errorx_test.go index f37ff98..e9d37e1 100644 --- a/internal/engine/netx/errorx/errorx_test.go +++ b/internal/engine/netx/errorx/errorx_test.go @@ -50,34 +50,11 @@ func TestToFailureString(t *testing.T) { t.Fatal("unexpected result") } }) - t.Run("for ErrDNSBogon", func(t *testing.T) { - if toFailureString(ErrDNSBogon) != FailureDNSBogonError { - t.Fatal("unexpected result") - } - }) t.Run("for context.Canceled", func(t *testing.T) { if toFailureString(context.Canceled) != FailureInterrupted { t.Fatal("unexpected result") } }) - t.Run("for x509.HostnameError", func(t *testing.T) { - var err x509.HostnameError - if toFailureString(err) != FailureSSLInvalidHostname { - t.Fatal("unexpected result") - } - }) - t.Run("for x509.UnknownAuthorityError", func(t *testing.T) { - var err x509.UnknownAuthorityError - if toFailureString(err) != FailureSSLUnknownAuthority { - t.Fatal("unexpected result") - } - }) - t.Run("for x509.CertificateInvalidError", func(t *testing.T) { - var err x509.CertificateInvalidError - if toFailureString(err) != FailureSSLInvalidCertificate { - t.Fatal("unexpected result") - } - }) t.Run("for operation was canceled error", func(t *testing.T) { if toFailureString(errors.New("operation was canceled")) != FailureInterrupted { t.Fatal("unexpected result") @@ -88,6 +65,11 @@ func TestToFailureString(t *testing.T) { t.Fatal("unexpected results") } }) + t.Run("for canceled", func(t *testing.T) { + if toFailureString(syscall.ECANCELED) != FailureInterrupted { + t.Fatal("unexpected results") + } + }) t.Run("for connection_refused", func(t *testing.T) { if toFailureString(syscall.ECONNREFUSED) != FailureConnectionRefused { t.Fatal("unexpected results") @@ -98,6 +80,16 @@ func TestToFailureString(t *testing.T) { t.Fatal("unexpected results") } }) + t.Run("for host_unreachable", func(t *testing.T) { + if toFailureString(syscall.EHOSTUNREACH) != FailureHostUnreachable { + t.Fatal("unexpected results") + } + }) + t.Run("for system timeout", func(t *testing.T) { + if toFailureString(syscall.ETIMEDOUT) != FailureGenericTimeoutError { + t.Fatal("unexpected results") + } + }) t.Run("for context deadline exceeded", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1) defer cancel() @@ -154,22 +146,6 @@ func TestToFailureString(t *testing.T) { t.Fatal(cmp.Diff(expected, out)) } }) - // QUIC failures - t.Run("for connection_refused", func(t *testing.T) { - if toFailureString(errors.New("connection_refused")) != FailureConnectionRefused { - t.Fatal("unexpected results") - } - }) - t.Run("for connection_reset", func(t *testing.T) { - if toFailureString(errors.New("stateless_reset")) != FailureConnectionReset { - t.Fatal("unexpected results") - } - }) - t.Run("for incompatible quic version", func(t *testing.T) { - if toFailureString(errors.New("No compatible QUIC version found")) != FailureNoCompatibleQUICVersion { - t.Fatal("unexpected results") - } - }) t.Run("for i/o error", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1) defer cancel() // fail immediately @@ -189,12 +165,88 @@ func TestToFailureString(t *testing.T) { t.Fatal("unexpected results") } }) - t.Run("for QUIC handshake timeout error", func(t *testing.T) { - err := errors.New("Handshake did not complete in time") - if toFailureString(err) != FailureGenericTimeoutError { +} + +func TestClassifyQUICFailure(t *testing.T) { + t.Run("for connection_reset", func(t *testing.T) { + if ClassifyQUICFailure(&quic.StatelessResetError{}) != FailureConnectionReset { t.Fatal("unexpected results") } }) + t.Run("for incompatible quic version", func(t *testing.T) { + 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 { + t.Fatal("unexpected results") + } + }) + t.Run("for quic handshake timeout", func(t *testing.T) { + 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 { + 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 { + 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 { + 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 { + 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 { + t.Fatal("unexpected results") + } + }) + +} + +func TestClassifyResolveFailure(t *testing.T) { + t.Run("for ErrDNSBogon", func(t *testing.T) { + if ClassifyResolveFailure(ErrDNSBogon) != FailureDNSBogonError { + t.Fatal("unexpected result") + } + }) +} + +func TestClassifyTLSFailure(t *testing.T) { + t.Run("for x509.HostnameError", func(t *testing.T) { + var err x509.HostnameError + 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 { + t.Fatal("unexpected result") + } + }) + t.Run("for x509.CertificateInvalidError", func(t *testing.T) { + var err x509.CertificateInvalidError + if ClassifyTLSFailure(err) != FailureSSLInvalidCertificate { + t.Fatal("unexpected result") + } + }) } func TestToOperationString(t *testing.T) { diff --git a/internal/engine/netx/quicdialer/errorwrapper.go b/internal/engine/netx/quicdialer/errorwrapper.go index cd6a2db..181b4e8 100644 --- a/internal/engine/netx/quicdialer/errorwrapper.go +++ b/internal/engine/netx/quicdialer/errorwrapper.go @@ -23,9 +23,10 @@ func (d ErrorWrapperDialer) DialContext( err = errorx.SafeErrWrapperBuilder{ // ConnID does not make any sense if we've failed and the error // does not make any sense (and is nil) if we succeeded. - DialID: dialID, - Error: err, - Operation: errorx.QUICHandshakeOperation, + Classifier: errorx.ClassifyQUICFailure, + DialID: dialID, + Error: err, + Operation: errorx.QUICHandshakeOperation, }.MaybeBuild() if err != nil { return nil, err diff --git a/internal/engine/netx/quicdialer/errorwrapper_test.go b/internal/engine/netx/quicdialer/errorwrapper_test.go index be500de..663492d 100644 --- a/internal/engine/netx/quicdialer/errorwrapper_test.go +++ b/internal/engine/netx/quicdialer/errorwrapper_test.go @@ -44,6 +44,29 @@ func errorWrapperCheckErr(t *testing.T, err error, op string) { } } +func TestErrorWrapperInvalidCertificate(t *testing.T) { + nextprotos := []string{"h3"} + servername := "example.com" + tlsConf := &tls.Config{ + NextProtos: nextprotos, + ServerName: servername, + } + + dlr := quicdialer.ErrorWrapperDialer{Dialer: &quicdialer.SystemDialer{}} + // use Google IP + sess, err := dlr.DialContext(context.Background(), "udp", + "216.58.212.164:443", tlsConf, &quic.Config{}) + if err == nil { + t.Fatal("expected an error here") + } + if sess != nil { + t.Fatal("expected nil sess here") + } + if err.Error() != errorx.FailureSSLInvalidCertificate { + t.Fatal("unexpected failure") + } +} + func TestErrorWrapperSuccess(t *testing.T) { ctx := dialid.WithDialID(context.Background()) tlsConf := &tls.Config{ diff --git a/internal/engine/netx/quicdialer/saver_test.go b/internal/engine/netx/quicdialer/saver_test.go index c5d5a50..1e93657 100644 --- a/internal/engine/netx/quicdialer/saver_test.go +++ b/internal/engine/netx/quicdialer/saver_test.go @@ -85,7 +85,7 @@ func TestHandshakeSaverSuccess(t *testing.T) { func TestHandshakeSaverHostNameError(t *testing.T) { nextprotos := []string{"h3"} - servername := "wrong.host.badssl.com" + servername := "example.com" tlsConf := &tls.Config{ NextProtos: nextprotos, ServerName: servername, diff --git a/internal/engine/netx/resolver/errorwrapper.go b/internal/engine/netx/resolver/errorwrapper.go index 1699477..e3285b2 100644 --- a/internal/engine/netx/resolver/errorwrapper.go +++ b/internal/engine/netx/resolver/errorwrapper.go @@ -19,6 +19,7 @@ func (r ErrorWrapperResolver) LookupHost(ctx context.Context, hostname string) ( txID := transactionid.ContextTransactionID(ctx) addrs, err := r.Resolver.LookupHost(ctx, hostname) err = errorx.SafeErrWrapperBuilder{ + Classifier: errorx.ClassifyResolveFailure, DialID: dialID, Error: err, Operation: errorx.ResolveOperation, diff --git a/internal/engine/netx/tlsdialer/tls.go b/internal/engine/netx/tlsdialer/tls.go index ccd76fa..2171492 100644 --- a/internal/engine/netx/tlsdialer/tls.go +++ b/internal/engine/netx/tlsdialer/tls.go @@ -71,9 +71,10 @@ func (h ErrorWrapperTLSHandshaker) Handshake( connID := connid.Compute(conn.RemoteAddr().Network(), conn.RemoteAddr().String()) tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config) err = errorx.SafeErrWrapperBuilder{ - ConnID: connID, - Error: err, - Operation: errorx.TLSHandshakeOperation, + Classifier: errorx.ClassifyTLSFailure, + ConnID: connID, + Error: err, + Operation: errorx.TLSHandshakeOperation, }.MaybeBuild() return tlsconn, state, err }