83440cf110
The legacy part for now is internal/errorsx. It will stay there until I figure out whether it also needs some extra bug fixing. The good part is now in internal/netxlite/errorsx and contains all the logic for mapping errors. We need to further improve upon this logic by writing more thorough integration tests for QUIC. We also need to copy the various dialer, conn, etc adapters that set errors. We will put them inside netxlite and we will generate errors in a way that is less crazy with respect to the major operation. (The idea is to always wrap, given that now we measure in an incremental way and we don't measure every operation together.) Part of https://github.com/ooni/probe/issues/1591
198 lines
6.5 KiB
Go
198 lines
6.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"
|
|
)
|
|
|
|
// 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.
|
|
|
|
// ClassifyGenericError is the generic classifier mapping an error
|
|
// occurred during an operation to an OONI failure string.
|
|
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
|
|
}
|
|
|
|
if failure := classifySyscallError(err); failure != "" {
|
|
return failure
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// ClassifyQUICHandshakeError maps an error occurred during the QUIC
|
|
// handshake to an OONI failure string.
|
|
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
|
|
var statelessReset *quic.StatelessResetError
|
|
var handshakeTimeout *quic.HandshakeTimeoutError
|
|
var idleTimeout *quic.IdleTimeoutError
|
|
var 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)
|
|
}
|
|
|
|
// 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.
|
|
var ErrDNSBogon = errors.New("dns: detected bogon address")
|
|
|
|
// ClassifyResolverError maps an error occurred during a domain name
|
|
// resolution to the corresponding OONI failure string.
|
|
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.
|
|
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)
|
|
}
|