1472f7530b
Later we will try to write comprehensive integration tests for the whole netxlite package. We want just unit tests here.
263 lines
8.5 KiB
Go
263 lines
8.5 KiB
Go
package errorsx
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/lucas-clemente/quic-go"
|
|
"github.com/ooni/probe-cli/v3/internal/scrubber"
|
|
)
|
|
|
|
// ClassifyGenericError is the generic classifier mapping an error
|
|
// occurred during an operation to an OONI failure string.
|
|
//
|
|
// If the input error is already an ErrWrapper we don't perform
|
|
// the classification again and we return its Failure to the caller.
|
|
//
|
|
// Classification rules
|
|
//
|
|
// We put inside this classifier:
|
|
//
|
|
// - system call errors
|
|
//
|
|
// - generic errors that can occur in multiple places
|
|
//
|
|
// - all the errors that depend on strings
|
|
//
|
|
// The more specific classifiers will call this classifier if
|
|
// they fail to find a mapping for the input error.
|
|
//
|
|
// If everything else fails, this classifier returns a string
|
|
// like "unknown_failure: XXX" where XXX has been scrubbed
|
|
// so to remove any network endpoints from its value.
|
|
func ClassifyGenericError(err error) string {
|
|
// The list returned here matches the values used by MK unless
|
|
// explicitly noted otherwise with a comment.
|
|
|
|
var errwrapper *ErrWrapper
|
|
if errors.As(err, &errwrapper) {
|
|
return errwrapper.Error() // we've already wrapped it
|
|
}
|
|
|
|
// Classify system errors first. We could use strings for many
|
|
// of them on Unix, but this would fail on Windows as described
|
|
// by https://github.com/ooni/probe/issues/1526.
|
|
if failure := classifySyscallError(err); failure != "" {
|
|
return failure
|
|
}
|
|
|
|
if errors.Is(err, context.Canceled) {
|
|
return FailureInterrupted
|
|
}
|
|
|
|
if failure := classifyWithStringSuffix(err); failure != "" {
|
|
return failure
|
|
}
|
|
|
|
formatted := fmt.Sprintf("unknown_failure: %s", err.Error())
|
|
return scrubber.Scrub(formatted) // scrub IP addresses in the error
|
|
}
|
|
|
|
// classifyWithStringSuffix is a subset of ClassifyGenericError that
|
|
// performs classification by looking at error suffixes. This function
|
|
// will return an empty string if it cannot classify the error.
|
|
func classifyWithStringSuffix(err error) string {
|
|
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
|
|
}
|
|
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
|
|
}
|
|
return "" // not found
|
|
}
|
|
|
|
// TLS alert protocol as defined in RFC8446. We need these definitions
|
|
// to figure out which error occurred during a QUIC handshake.
|
|
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
|
|
)
|
|
|
|
// ClassifyQUICHandshakeError maps an error occurred during the QUIC
|
|
// handshake to an OONI failure string.
|
|
//
|
|
// If the input error is already an ErrWrapper we don't perform
|
|
// the classification again and we return its Failure to the caller.
|
|
//
|
|
// If this classifier fails, it calls ClassifyGenericError and
|
|
// returns to the caller its return value.
|
|
func ClassifyQUICHandshakeError(err error) string {
|
|
var errwrapper *ErrWrapper
|
|
if errors.As(err, &errwrapper) {
|
|
return errwrapper.Error() // we've already wrapped it
|
|
}
|
|
|
|
var (
|
|
versionNegotiation *quic.VersionNegotiationError
|
|
statelessReset *quic.StatelessResetError
|
|
handshakeTimeout *quic.HandshakeTimeoutError
|
|
idleTimeout *quic.IdleTimeoutError
|
|
transportError *quic.TransportError
|
|
)
|
|
|
|
if errors.As(err, &versionNegotiation) {
|
|
return FailureQUICIncompatibleVersion
|
|
}
|
|
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 FailureSSLFailedHandshake
|
|
}
|
|
if errCode == quicTLSAlertUnknownCA {
|
|
return FailureSSLUnknownAuthority
|
|
}
|
|
if errCode == quicTLSUnrecognizedName {
|
|
return FailureSSLInvalidHostname
|
|
}
|
|
}
|
|
return ClassifyGenericError(err)
|
|
}
|
|
|
|
// quicIsCertificateError tells us whether a specific TLS alert error
|
|
// we received is actually an error depending on the certificate.
|
|
//
|
|
// The set of checks we implement here is a set of heuristics based
|
|
// on our understanding of the TLS spec and may need tweaks.
|
|
func quicIsCertificateError(alert uint8) bool {
|
|
// List out each case separately so we know we test them
|
|
switch alert {
|
|
case quicTLSAlertBadCertificate:
|
|
return true
|
|
case quicTLSAlertUnsupportedCertificate:
|
|
return true
|
|
case quicTLSAlertCertificateExpired:
|
|
return true
|
|
case quicTLSAlertCertificateRevoked:
|
|
return true
|
|
case quicTLSAlertCertificateUnknown:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ErrDNSBogon indicates that we found a bogon address. Code that
|
|
// filters for DNS bogons MUST use this error.
|
|
var ErrDNSBogon = errors.New("dns: detected bogon address")
|
|
|
|
// ClassifyResolverError maps an error occurred during a domain name
|
|
// resolution to the corresponding OONI failure string.
|
|
//
|
|
// If the input error is already an ErrWrapper we don't perform
|
|
// the classification again and we return its Failure to the caller.
|
|
//
|
|
// If this classifier fails, it calls ClassifyGenericError and
|
|
// returns to the caller its return value.
|
|
func ClassifyResolverError(err error) string {
|
|
var errwrapper *ErrWrapper
|
|
if errors.As(err, &errwrapper) {
|
|
return errwrapper.Error() // we've already wrapped it
|
|
}
|
|
if errors.Is(err, ErrDNSBogon) {
|
|
return FailureDNSBogonError // not in MK
|
|
}
|
|
return ClassifyGenericError(err)
|
|
}
|
|
|
|
// ClassifyTLSHandshakeError maps an error occurred during the TLS
|
|
// handshake to an OONI failure string.
|
|
//
|
|
// If the input error is already an ErrWrapper we don't perform
|
|
// the classification again and we return its Failure to the caller.
|
|
//
|
|
// If this classifier fails, it calls ClassifyGenericError and
|
|
// returns to the caller its return value.
|
|
func ClassifyTLSHandshakeError(err error) string {
|
|
var errwrapper *ErrWrapper
|
|
if errors.As(err, &errwrapper) {
|
|
return errwrapper.Error() // we've already wrapped it
|
|
}
|
|
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 ClassifyGenericError(err)
|
|
}
|