cli: error classification refactoring (#386)
* make errorx classifier less dependent on strings * adapt errorx tests * added syserror comment * localized classification of quic errors * localized classification of resolver errors * (fix) move "no such host" error to global classifier * moved x509 errors to local TLS error classifier * added qtls error classification for quicdialer * add Classifier to SafeErrWrapperBuilder * windows/unix specific files for errno constants * added errno ETIMEDOUT, tests * added TLS alert constants * added FailureSSLHandshake test, improved switch style * added more network based system error constants for future use * (fix) import style * (fix) errorx typos/style * (fix) robustness of SafeErrWrapperBuilder, added comments * (fix) reversed unnecessary changes, added comments * (fix) style and updated comment * errorx: added future re-structuring comment * (fix) typo TLS alert code 51 * added comment * alert mapping: added comment * Update errorx.go * Update internal/engine/netx/errorx/errorx.go Co-authored-by: Simone Basso <bassosimone@gmail.com>
This commit is contained in:
parent
1eb6e758c6
commit
1fefe5d9b8
34
internal/engine/netx/errorx/errno_unix.go
Normal file
34
internal/engine/netx/errorx/errno_unix.go
Normal file
@ -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
|
||||||
|
)
|
34
internal/engine/netx/errorx/errno_windows.go
Normal file
34
internal/engine/netx/errorx/errno_windows.go
Normal file
@ -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
|
||||||
|
)
|
@ -1,14 +1,22 @@
|
|||||||
// Package errorx contains error extensions
|
// Package errorx contains error extensions
|
||||||
package errorx
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/lucas-clemente/quic-go"
|
||||||
"github.com/ooni/probe-cli/v3/internal/scrubber"
|
"github.com/ooni/probe-cli/v3/internal/scrubber"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,12 +39,18 @@ const (
|
|||||||
// FailureGenericTimeoutError means we got some timer has expired.
|
// FailureGenericTimeoutError means we got some timer has expired.
|
||||||
FailureGenericTimeoutError = "generic_timeout_error"
|
FailureGenericTimeoutError = "generic_timeout_error"
|
||||||
|
|
||||||
|
// FailureHostUnreachable means that there is "no route to host".
|
||||||
|
FailureHostUnreachable = "host_unreachable"
|
||||||
|
|
||||||
// FailureInterrupted means that the user interrupted us.
|
// FailureInterrupted means that the user interrupted us.
|
||||||
FailureInterrupted = "interrupted"
|
FailureInterrupted = "interrupted"
|
||||||
|
|
||||||
// FailureNoCompatibleQUICVersion means that the server does not support the proposed QUIC version
|
// FailureNoCompatibleQUICVersion means that the server does not support the proposed QUIC version
|
||||||
FailureNoCompatibleQUICVersion = "quic_incompatible_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 means we got certificate is not valid for SNI.
|
||||||
FailureSSLInvalidHostname = "ssl_invalid_hostname"
|
FailureSSLInvalidHostname = "ssl_invalid_hostname"
|
||||||
|
|
||||||
@ -51,6 +65,36 @@ const (
|
|||||||
FailureJSONParseError = "json_parse_error"
|
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 (
|
const (
|
||||||
// ResolveOperation is the operation where we resolve a domain name
|
// ResolveOperation is the operation where we resolve a domain name
|
||||||
ResolveOperation = "resolve"
|
ResolveOperation = "resolve"
|
||||||
@ -166,6 +210,10 @@ type SafeErrWrapperBuilder struct {
|
|||||||
// Error is the error, if any
|
// Error is the error, if any
|
||||||
Error error
|
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 is the operation that failed
|
||||||
Operation string
|
Operation string
|
||||||
|
|
||||||
@ -177,10 +225,14 @@ type SafeErrWrapperBuilder struct {
|
|||||||
// a nil error value, instead, if b.Error is nil.
|
// a nil error value, instead, if b.Error is nil.
|
||||||
func (b SafeErrWrapperBuilder) MaybeBuild() (err error) {
|
func (b SafeErrWrapperBuilder) MaybeBuild() (err error) {
|
||||||
if b.Error != nil {
|
if b.Error != nil {
|
||||||
|
classifier := b.Classifier
|
||||||
|
if classifier == nil {
|
||||||
|
classifier = toFailureString
|
||||||
|
}
|
||||||
err = &ErrWrapper{
|
err = &ErrWrapper{
|
||||||
ConnID: b.ConnID,
|
ConnID: b.ConnID,
|
||||||
DialID: b.DialID,
|
DialID: b.DialID,
|
||||||
Failure: toFailureString(b.Error),
|
Failure: classifier(b.Error),
|
||||||
Operation: toOperationString(b.Error, b.Operation),
|
Operation: toOperationString(b.Error, b.Operation),
|
||||||
TransactionID: b.TransactionID,
|
TransactionID: b.TransactionID,
|
||||||
WrappedErr: b.Error,
|
WrappedErr: b.Error,
|
||||||
@ -189,6 +241,10 @@ func (b SafeErrWrapperBuilder) MaybeBuild() (err error) {
|
|||||||
return
|
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 {
|
func toFailureString(err error) string {
|
||||||
// The list returned here matches the values used by MK unless
|
// The list returned here matches the values used by MK unless
|
||||||
// explicitly noted otherwise with a comment.
|
// explicitly noted otherwise with a comment.
|
||||||
@ -198,12 +254,114 @@ func toFailureString(err error) string {
|
|||||||
return errwrapper.Error() // we've already wrapped it
|
return errwrapper.Error() // we've already wrapped it
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, ErrDNSBogon) {
|
// filter out system errors: necessary to detect all windows errors
|
||||||
return FailureDNSBogonError // not in MK
|
// 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) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return FailureInterrupted
|
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
|
var x509HostnameError x509.HostnameError
|
||||||
if errors.As(err, &x509HostnameError) {
|
if errors.As(err, &x509HostnameError) {
|
||||||
// Test case: https://wrong.host.badssl.com/
|
// Test case: https://wrong.host.badssl.com/
|
||||||
@ -220,76 +378,7 @@ func toFailureString(err error) string {
|
|||||||
// Test case: https://expired.badssl.com/
|
// Test case: https://expired.badssl.com/
|
||||||
return FailureSSLInvalidCertificate
|
return FailureSSLInvalidCertificate
|
||||||
}
|
}
|
||||||
|
return toFailureString(err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toOperationString(err error, operation string) string {
|
func toOperationString(err error, operation string) string {
|
||||||
@ -322,3 +411,11 @@ func toOperationString(err error, operation string) string {
|
|||||||
}
|
}
|
||||||
return operation
|
return operation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCertificateError(alert uint8) bool {
|
||||||
|
return (alert == TLSAlertBadCertificate ||
|
||||||
|
alert == TLSAlertUnsupportedCertificate ||
|
||||||
|
alert == TLSAlertCertificateExpired ||
|
||||||
|
alert == TLSAlertCertificateRevoked ||
|
||||||
|
alert == TLSAlertCertificateUnknown)
|
||||||
|
}
|
||||||
|
@ -50,34 +50,11 @@ func TestToFailureString(t *testing.T) {
|
|||||||
t.Fatal("unexpected result")
|
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) {
|
t.Run("for context.Canceled", func(t *testing.T) {
|
||||||
if toFailureString(context.Canceled) != FailureInterrupted {
|
if toFailureString(context.Canceled) != FailureInterrupted {
|
||||||
t.Fatal("unexpected result")
|
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) {
|
t.Run("for operation was canceled error", func(t *testing.T) {
|
||||||
if toFailureString(errors.New("operation was canceled")) != FailureInterrupted {
|
if toFailureString(errors.New("operation was canceled")) != FailureInterrupted {
|
||||||
t.Fatal("unexpected result")
|
t.Fatal("unexpected result")
|
||||||
@ -88,6 +65,11 @@ func TestToFailureString(t *testing.T) {
|
|||||||
t.Fatal("unexpected results")
|
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) {
|
t.Run("for connection_refused", func(t *testing.T) {
|
||||||
if toFailureString(syscall.ECONNREFUSED) != FailureConnectionRefused {
|
if toFailureString(syscall.ECONNREFUSED) != FailureConnectionRefused {
|
||||||
t.Fatal("unexpected results")
|
t.Fatal("unexpected results")
|
||||||
@ -98,6 +80,16 @@ func TestToFailureString(t *testing.T) {
|
|||||||
t.Fatal("unexpected results")
|
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) {
|
t.Run("for context deadline exceeded", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1)
|
ctx, cancel := context.WithTimeout(context.Background(), 1)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -154,22 +146,6 @@ func TestToFailureString(t *testing.T) {
|
|||||||
t.Fatal(cmp.Diff(expected, out))
|
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) {
|
t.Run("for i/o error", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1)
|
ctx, cancel := context.WithTimeout(context.Background(), 1)
|
||||||
defer cancel() // fail immediately
|
defer cancel() // fail immediately
|
||||||
@ -189,12 +165,88 @@ func TestToFailureString(t *testing.T) {
|
|||||||
t.Fatal("unexpected results")
|
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.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) {
|
func TestToOperationString(t *testing.T) {
|
||||||
|
@ -23,9 +23,10 @@ func (d ErrorWrapperDialer) DialContext(
|
|||||||
err = errorx.SafeErrWrapperBuilder{
|
err = errorx.SafeErrWrapperBuilder{
|
||||||
// ConnID does not make any sense if we've failed and the error
|
// ConnID does not make any sense if we've failed and the error
|
||||||
// does not make any sense (and is nil) if we succeeded.
|
// does not make any sense (and is nil) if we succeeded.
|
||||||
DialID: dialID,
|
Classifier: errorx.ClassifyQUICFailure,
|
||||||
Error: err,
|
DialID: dialID,
|
||||||
Operation: errorx.QUICHandshakeOperation,
|
Error: err,
|
||||||
|
Operation: errorx.QUICHandshakeOperation,
|
||||||
}.MaybeBuild()
|
}.MaybeBuild()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -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) {
|
func TestErrorWrapperSuccess(t *testing.T) {
|
||||||
ctx := dialid.WithDialID(context.Background())
|
ctx := dialid.WithDialID(context.Background())
|
||||||
tlsConf := &tls.Config{
|
tlsConf := &tls.Config{
|
||||||
|
@ -85,7 +85,7 @@ func TestHandshakeSaverSuccess(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandshakeSaverHostNameError(t *testing.T) {
|
func TestHandshakeSaverHostNameError(t *testing.T) {
|
||||||
nextprotos := []string{"h3"}
|
nextprotos := []string{"h3"}
|
||||||
servername := "wrong.host.badssl.com"
|
servername := "example.com"
|
||||||
tlsConf := &tls.Config{
|
tlsConf := &tls.Config{
|
||||||
NextProtos: nextprotos,
|
NextProtos: nextprotos,
|
||||||
ServerName: servername,
|
ServerName: servername,
|
||||||
|
@ -19,6 +19,7 @@ func (r ErrorWrapperResolver) LookupHost(ctx context.Context, hostname string) (
|
|||||||
txID := transactionid.ContextTransactionID(ctx)
|
txID := transactionid.ContextTransactionID(ctx)
|
||||||
addrs, err := r.Resolver.LookupHost(ctx, hostname)
|
addrs, err := r.Resolver.LookupHost(ctx, hostname)
|
||||||
err = errorx.SafeErrWrapperBuilder{
|
err = errorx.SafeErrWrapperBuilder{
|
||||||
|
Classifier: errorx.ClassifyResolveFailure,
|
||||||
DialID: dialID,
|
DialID: dialID,
|
||||||
Error: err,
|
Error: err,
|
||||||
Operation: errorx.ResolveOperation,
|
Operation: errorx.ResolveOperation,
|
||||||
|
@ -71,9 +71,10 @@ func (h ErrorWrapperTLSHandshaker) Handshake(
|
|||||||
connID := connid.Compute(conn.RemoteAddr().Network(), conn.RemoteAddr().String())
|
connID := connid.Compute(conn.RemoteAddr().Network(), conn.RemoteAddr().String())
|
||||||
tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config)
|
tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config)
|
||||||
err = errorx.SafeErrWrapperBuilder{
|
err = errorx.SafeErrWrapperBuilder{
|
||||||
ConnID: connID,
|
Classifier: errorx.ClassifyTLSFailure,
|
||||||
Error: err,
|
ConnID: connID,
|
||||||
Operation: errorx.TLSHandshakeOperation,
|
Error: err,
|
||||||
|
Operation: errorx.TLSHandshakeOperation,
|
||||||
}.MaybeBuild()
|
}.MaybeBuild()
|
||||||
return tlsconn, state, err
|
return tlsconn, state, err
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user