refactor: split errorsx in good and legacy (#477)

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
This commit is contained in:
Simone Basso
2021-09-07 17:09:30 +02:00
committed by GitHub
parent ccb3a644e1
commit 83440cf110
70 changed files with 684 additions and 576 deletions
+197
View File
@@ -0,0 +1,197 @@
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)
}
+260
View File
@@ -0,0 +1,260 @@
package errorsx
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"syscall"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/lucas-clemente/quic-go"
"github.com/pion/stun"
)
func TestClassifyGenericError(t *testing.T) {
t.Run("for input being already an ErrWrapper", func(t *testing.T) {
err := &ErrWrapper{Failure: FailureEOFError}
if ClassifyGenericError(err) != FailureEOFError {
t.Fatal("did not classify existing ErrWrapper correctly")
}
})
t.Run("for already wrapped error", func(t *testing.T) {
err := io.EOF
if ClassifyGenericError(err) != FailureEOFError {
t.Fatal("unexpected result")
}
})
t.Run("for context.Canceled", func(t *testing.T) {
if ClassifyGenericError(context.Canceled) != FailureInterrupted {
t.Fatal("unexpected result")
}
})
t.Run("for operation was canceled error", func(t *testing.T) {
if ClassifyGenericError(errors.New("operation was canceled")) != FailureInterrupted {
t.Fatal("unexpected result")
}
})
t.Run("for EOF", func(t *testing.T) {
if ClassifyGenericError(io.EOF) != FailureEOFError {
t.Fatal("unexpected results")
}
})
t.Run("for canceled", func(t *testing.T) {
if ClassifyGenericError(syscall.ECANCELED) != FailureOperationCanceled {
t.Fatal("unexpected results")
}
})
t.Run("for connection_refused", func(t *testing.T) {
if ClassifyGenericError(syscall.ECONNREFUSED) != FailureConnectionRefused {
t.Fatal("unexpected results")
}
})
t.Run("for connection_reset", func(t *testing.T) {
if ClassifyGenericError(syscall.ECONNRESET) != FailureConnectionReset {
t.Fatal("unexpected results")
}
})
t.Run("for host_unreachable", func(t *testing.T) {
if ClassifyGenericError(syscall.EHOSTUNREACH) != FailureHostUnreachable {
t.Fatal("unexpected results")
}
})
t.Run("for system timeout", func(t *testing.T) {
if ClassifyGenericError(syscall.ETIMEDOUT) != FailureTimedOut {
t.Fatal("unexpected results")
}
})
t.Run("for context deadline exceeded", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel()
<-ctx.Done()
if ClassifyGenericError(ctx.Err()) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for stun's transaction is timed out", func(t *testing.T) {
if ClassifyGenericError(stun.ErrTransactionTimeOut) != FailureGenericTimeoutError {
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
conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", "www.google.com:80")
if err == nil {
t.Fatal("expected an error here")
}
if conn != nil {
t.Fatal("expected nil connection here")
}
if ClassifyGenericError(err) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for TLS handshake timeout error", func(t *testing.T) {
err := errors.New("net/http: TLS handshake timeout")
if ClassifyGenericError(err) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for no such host", func(t *testing.T) {
if ClassifyGenericError(&net.DNSError{
Err: "no such host",
}) != FailureDNSNXDOMAINError {
t.Fatal("unexpected results")
}
})
t.Run("for errors including IPv4 address", func(t *testing.T) {
input := errors.New("read tcp 10.0.2.15:56948->93.184.216.34:443: use of closed network connection")
expected := "unknown_failure: read tcp [scrubbed]->[scrubbed]: use of closed network connection"
out := ClassifyGenericError(input)
if out != expected {
t.Fatal(cmp.Diff(expected, out))
}
})
t.Run("for errors including IPv6 address", func(t *testing.T) {
input := errors.New("read tcp [::1]:56948->[::1]:443: use of closed network connection")
expected := "unknown_failure: read tcp [scrubbed]->[scrubbed]: use of closed network connection"
out := ClassifyGenericError(input)
if out != expected {
t.Fatal(cmp.Diff(expected, out))
}
})
t.Run("for i/o error", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel() // fail immediately
udpAddr := &net.UDPAddr{IP: net.ParseIP("216.58.212.164"), Port: 80, Zone: ""}
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
t.Fatal(err)
}
sess, err := quic.DialEarlyContext(ctx, udpConn, udpAddr, "google.com:80", &tls.Config{}, &quic.Config{})
if err == nil {
t.Fatal("expected an error here")
}
if sess != nil {
t.Fatal("expected nil session here")
}
if ClassifyGenericError(err) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
}
func TestClassifyQUICHandshakeError(t *testing.T) {
t.Run("for input being already an ErrWrapper", func(t *testing.T) {
err := &ErrWrapper{Failure: FailureEOFError}
if ClassifyQUICHandshakeError(err) != FailureEOFError {
t.Fatal("did not classify existing ErrWrapper correctly")
}
})
t.Run("for connection_reset", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.StatelessResetError{}) != FailureConnectionReset {
t.Fatal("unexpected results")
}
})
t.Run("for incompatible quic version", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.VersionNegotiationError{}) != FailureQUICIncompatibleVersion {
t.Fatal("unexpected results")
}
})
t.Run("for quic connection refused", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: quic.ConnectionRefused}) != FailureConnectionRefused {
t.Fatal("unexpected results")
}
})
t.Run("for quic handshake timeout", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.HandshakeTimeoutError{}) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC idle connection timeout", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.IdleTimeoutError{}) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC CRYPTO Handshake", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertHandshakeFailure
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLFailedHandshake {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC CRYPTO Invalid Certificate", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertBadCertificate
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidCertificate {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC CRYPTO Unknown CA", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertUnknownCA
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLUnknownAuthority {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC CRYPTO Bad Hostname", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSUnrecognizedName
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidHostname {
t.Fatal("unexpected results")
}
})
t.Run("for another kind of error", func(t *testing.T) {
if ClassifyQUICHandshakeError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result")
}
})
}
func TestClassifyResolverError(t *testing.T) {
t.Run("for input being already an ErrWrapper", func(t *testing.T) {
err := &ErrWrapper{Failure: FailureEOFError}
if ClassifyResolverError(err) != FailureEOFError {
t.Fatal("did not classify existing ErrWrapper correctly")
}
})
t.Run("for ErrDNSBogon", func(t *testing.T) {
if ClassifyResolverError(ErrDNSBogon) != FailureDNSBogonError {
t.Fatal("unexpected result")
}
})
t.Run("for another kind of error", func(t *testing.T) {
if ClassifyResolverError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result")
}
})
}
func TestClassifyTLSHandshakeError(t *testing.T) {
t.Run("for input being already an ErrWrapper", func(t *testing.T) {
err := &ErrWrapper{Failure: FailureEOFError}
if ClassifyTLSHandshakeError(err) != FailureEOFError {
t.Fatal("did not classify existing ErrWrapper correctly")
}
})
t.Run("for x509.HostnameError", func(t *testing.T) {
var err x509.HostnameError
if ClassifyTLSHandshakeError(err) != FailureSSLInvalidHostname {
t.Fatal("unexpected result")
}
})
t.Run("for x509.UnknownAuthorityError", func(t *testing.T) {
var err x509.UnknownAuthorityError
if ClassifyTLSHandshakeError(err) != FailureSSLUnknownAuthority {
t.Fatal("unexpected result")
}
})
t.Run("for x509.CertificateInvalidError", func(t *testing.T) {
var err x509.CertificateInvalidError
if ClassifyTLSHandshakeError(err) != FailureSSLInvalidCertificate {
t.Fatal("unexpected result")
}
})
t.Run("for another kind of error", func(t *testing.T) {
if ClassifyTLSHandshakeError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result")
}
})
}
+2
View File
@@ -0,0 +1,2 @@
// Package errorsx contains code to classify errors.
package errorsx
+133
View File
@@ -0,0 +1,133 @@
// Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.462721 +0200 CEST m=+0.105415376
package errorsx
//go:generate go run ./internal/generrno/
import (
"errors"
"syscall"
)
// This enumeration lists the failures defined at
// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md
const (
//
// System errors
//
FailureOperationCanceled = "operation_canceled"
FailureConnectionRefused = "connection_refused"
FailureConnectionReset = "connection_reset"
FailureHostUnreachable = "host_unreachable"
FailureTimedOut = "timed_out"
FailureAddressFamilyNotSupported = "address_family_not_supported"
FailureAddressInUse = "address_in_use"
FailureAddressNotAvailable = "address_not_available"
FailureAlreadyConnected = "already_connected"
FailureBadAddress = "bad_address"
FailureBadFileDescriptor = "bad_file_descriptor"
FailureConnectionAborted = "connection_aborted"
FailureConnectionAlreadyInProgress = "connection_already_in_progress"
FailureDestinationAddressRequired = "destination_address_required"
FailureInterrupted = "interrupted"
FailureInvalidArgument = "invalid_argument"
FailureMessageSize = "message_size"
FailureNetworkDown = "network_down"
FailureNetworkReset = "network_reset"
FailureNetworkUnreachable = "network_unreachable"
FailureNoBufferSpace = "no_buffer_space"
FailureNoProtocolOption = "no_protocol_option"
FailureNotASocket = "not_a_socket"
FailureNotConnected = "not_connected"
FailureOperationWouldBlock = "operation_would_block"
FailurePermissionDenied = "permission_denied"
FailureProtocolNotSupported = "protocol_not_supported"
FailureWrongProtocolType = "wrong_protocol_type"
//
// Library errors
//
FailureDNSBogonError = "dns_bogon_error"
FailureDNSNXDOMAINError = "dns_nxdomain_error"
FailureEOFError = "eof_error"
FailureGenericTimeoutError = "generic_timeout_error"
FailureQUICIncompatibleVersion = "quic_incompatible_version"
FailureSSLFailedHandshake = "ssl_failed_handshake"
FailureSSLInvalidHostname = "ssl_invalid_hostname"
FailureSSLUnknownAuthority = "ssl_unknown_authority"
FailureSSLInvalidCertificate = "ssl_invalid_certificate"
FailureJSONParseError = "json_parse_error"
)
// classifySyscallError converts a syscall error to the
// proper OONI error. Returns the OONI error string
// on success, an empty string otherwise.
func classifySyscallError(err error) string {
// 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) {
return ""
}
switch errno {
case ECANCELED:
return FailureOperationCanceled
case ECONNREFUSED:
return FailureConnectionRefused
case ECONNRESET:
return FailureConnectionReset
case EHOSTUNREACH:
return FailureHostUnreachable
case ETIMEDOUT:
return FailureTimedOut
case EAFNOSUPPORT:
return FailureAddressFamilyNotSupported
case EADDRINUSE:
return FailureAddressInUse
case EADDRNOTAVAIL:
return FailureAddressNotAvailable
case EISCONN:
return FailureAlreadyConnected
case EFAULT:
return FailureBadAddress
case EBADF:
return FailureBadFileDescriptor
case ECONNABORTED:
return FailureConnectionAborted
case EALREADY:
return FailureConnectionAlreadyInProgress
case EDESTADDRREQ:
return FailureDestinationAddressRequired
case EINTR:
return FailureInterrupted
case EINVAL:
return FailureInvalidArgument
case EMSGSIZE:
return FailureMessageSize
case ENETDOWN:
return FailureNetworkDown
case ENETRESET:
return FailureNetworkReset
case ENETUNREACH:
return FailureNetworkUnreachable
case ENOBUFS:
return FailureNoBufferSpace
case ENOPROTOOPT:
return FailureNoProtocolOption
case ENOTSOCK:
return FailureNotASocket
case ENOTCONN:
return FailureNotConnected
case EWOULDBLOCK:
return FailureOperationWouldBlock
case EACCES:
return FailurePermissionDenied
case EPROTONOSUPPORT:
return FailureProtocolNotSupported
case EPROTOTYPE:
return FailureWrongProtocolType
}
return ""
}
+103
View File
@@ -0,0 +1,103 @@
// Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.510432 +0200 CEST m=+0.153127376
package errorsx
import (
"io"
"syscall"
"testing"
)
func TestToSyscallErr(t *testing.T) {
if v := classifySyscallError(io.EOF); v != "" {
t.Fatalf("expected empty string, got '%s'", v)
}
if v := classifySyscallError(ECANCELED); v != FailureOperationCanceled {
t.Fatalf("expected '%s', got '%s'", FailureOperationCanceled, v)
}
if v := classifySyscallError(ECONNREFUSED); v != FailureConnectionRefused {
t.Fatalf("expected '%s', got '%s'", FailureConnectionRefused, v)
}
if v := classifySyscallError(ECONNRESET); v != FailureConnectionReset {
t.Fatalf("expected '%s', got '%s'", FailureConnectionReset, v)
}
if v := classifySyscallError(EHOSTUNREACH); v != FailureHostUnreachable {
t.Fatalf("expected '%s', got '%s'", FailureHostUnreachable, v)
}
if v := classifySyscallError(ETIMEDOUT); v != FailureTimedOut {
t.Fatalf("expected '%s', got '%s'", FailureTimedOut, v)
}
if v := classifySyscallError(EAFNOSUPPORT); v != FailureAddressFamilyNotSupported {
t.Fatalf("expected '%s', got '%s'", FailureAddressFamilyNotSupported, v)
}
if v := classifySyscallError(EADDRINUSE); v != FailureAddressInUse {
t.Fatalf("expected '%s', got '%s'", FailureAddressInUse, v)
}
if v := classifySyscallError(EADDRNOTAVAIL); v != FailureAddressNotAvailable {
t.Fatalf("expected '%s', got '%s'", FailureAddressNotAvailable, v)
}
if v := classifySyscallError(EISCONN); v != FailureAlreadyConnected {
t.Fatalf("expected '%s', got '%s'", FailureAlreadyConnected, v)
}
if v := classifySyscallError(EFAULT); v != FailureBadAddress {
t.Fatalf("expected '%s', got '%s'", FailureBadAddress, v)
}
if v := classifySyscallError(EBADF); v != FailureBadFileDescriptor {
t.Fatalf("expected '%s', got '%s'", FailureBadFileDescriptor, v)
}
if v := classifySyscallError(ECONNABORTED); v != FailureConnectionAborted {
t.Fatalf("expected '%s', got '%s'", FailureConnectionAborted, v)
}
if v := classifySyscallError(EALREADY); v != FailureConnectionAlreadyInProgress {
t.Fatalf("expected '%s', got '%s'", FailureConnectionAlreadyInProgress, v)
}
if v := classifySyscallError(EDESTADDRREQ); v != FailureDestinationAddressRequired {
t.Fatalf("expected '%s', got '%s'", FailureDestinationAddressRequired, v)
}
if v := classifySyscallError(EINTR); v != FailureInterrupted {
t.Fatalf("expected '%s', got '%s'", FailureInterrupted, v)
}
if v := classifySyscallError(EINVAL); v != FailureInvalidArgument {
t.Fatalf("expected '%s', got '%s'", FailureInvalidArgument, v)
}
if v := classifySyscallError(EMSGSIZE); v != FailureMessageSize {
t.Fatalf("expected '%s', got '%s'", FailureMessageSize, v)
}
if v := classifySyscallError(ENETDOWN); v != FailureNetworkDown {
t.Fatalf("expected '%s', got '%s'", FailureNetworkDown, v)
}
if v := classifySyscallError(ENETRESET); v != FailureNetworkReset {
t.Fatalf("expected '%s', got '%s'", FailureNetworkReset, v)
}
if v := classifySyscallError(ENETUNREACH); v != FailureNetworkUnreachable {
t.Fatalf("expected '%s', got '%s'", FailureNetworkUnreachable, v)
}
if v := classifySyscallError(ENOBUFS); v != FailureNoBufferSpace {
t.Fatalf("expected '%s', got '%s'", FailureNoBufferSpace, v)
}
if v := classifySyscallError(ENOPROTOOPT); v != FailureNoProtocolOption {
t.Fatalf("expected '%s', got '%s'", FailureNoProtocolOption, v)
}
if v := classifySyscallError(ENOTSOCK); v != FailureNotASocket {
t.Fatalf("expected '%s', got '%s'", FailureNotASocket, v)
}
if v := classifySyscallError(ENOTCONN); v != FailureNotConnected {
t.Fatalf("expected '%s', got '%s'", FailureNotConnected, v)
}
if v := classifySyscallError(EWOULDBLOCK); v != FailureOperationWouldBlock {
t.Fatalf("expected '%s', got '%s'", FailureOperationWouldBlock, v)
}
if v := classifySyscallError(EACCES); v != FailurePermissionDenied {
t.Fatalf("expected '%s', got '%s'", FailurePermissionDenied, v)
}
if v := classifySyscallError(EPROTONOSUPPORT); v != FailureProtocolNotSupported {
t.Fatalf("expected '%s', got '%s'", FailureProtocolNotSupported, v)
}
if v := classifySyscallError(EPROTOTYPE); v != FailureWrongProtocolType {
t.Fatalf("expected '%s', got '%s'", FailureWrongProtocolType, v)
}
if v := classifySyscallError(syscall.Errno(0)); v != "" {
t.Fatalf("expected empty string, got '%s'", v)
}
}
+37
View File
@@ -0,0 +1,37 @@
// Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.35751 +0200 CEST m=+0.000202959
package errorsx
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
)
@@ -0,0 +1,37 @@
// Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.436584 +0200 CEST m=+0.079277834
package errorsx
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
)
+51
View File
@@ -0,0 +1,51 @@
package errorsx
// ErrWrapper is our error wrapper for Go errors. The key objective of
// this structure is to properly set Failure, which is also returned by
// the Error() method, so be one of the OONI defined strings.
type ErrWrapper struct {
// Failure is the OONI failure string. The failure strings are
// loosely backward compatible with Measurement Kit.
//
// This is either one of the FailureXXX strings or any other
// string like `unknown_failure ...`. The latter represents an
// error that we have not yet mapped to a failure.
Failure string
// Operation is the operation that failed. If possible, it
// SHOULD be a _major_ operation. Major operations are:
//
// - ResolveOperation: resolving a domain name failed
// - ConnectOperation: connecting to an IP failed
// - TLSHandshakeOperation: TLS handshaking failed
// - HTTPRoundTripOperation: other errors during round trip
//
// Because a network connection doesn't necessarily know
// what is the current major operation we also have the
// following _minor_ operations:
//
// - CloseOperation: CLOSE failed
// - ReadOperation: READ failed
// - WriteOperation: WRITE failed
//
// If an ErrWrapper referring to a major operation is wrapping
// another ErrWrapper and such ErrWrapper already refers to
// a major operation, then the new ErrWrapper should use the
// child ErrWrapper major operation. Otherwise, it should use
// its own major operation. This way, the topmost wrapper is
// supposed to refer to the major operation that failed.
Operation string
// WrappedErr is the error that we're wrapping.
WrappedErr error
}
// Error returns a description of the error that occurred.
func (e *ErrWrapper) Error() string {
return e.Failure
}
// Unwrap allows to access the underlying error
func (e *ErrWrapper) Unwrap() error {
return e.WrappedErr
}
@@ -0,0 +1,24 @@
package errorsx
import (
"errors"
"io"
"testing"
)
func TestErrWrapperError(t *testing.T) {
err := &ErrWrapper{Failure: FailureDNSNXDOMAINError}
if err.Error() != FailureDNSNXDOMAINError {
t.Fatal("invalid return value")
}
}
func TestErrWrapperUnwrap(t *testing.T) {
err := &ErrWrapper{
Failure: FailureEOFError,
WrappedErr: io.EOF,
}
if !errors.Is(err, io.EOF) {
t.Fatal("cannot unwrap error")
}
}
@@ -0,0 +1,267 @@
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/iancoleman/strcase"
"golang.org/x/sys/execabs"
)
// ErrorSpec specifies the error we care about.
type ErrorSpec struct {
// errno is the error name as an errno value (e.g., ECONNREFUSED).
errno string
// failure is the error name according to OONI (e.g., FailureConnectionRefused).
failure string
}
// AsErrnoName returns the name of the corresponding errno, if this
// is a system error, or panics otherwise.
func (es *ErrorSpec) AsErrnoName() string {
if !es.IsSystemError() {
panic("not a system error")
}
return es.errno
}
// AsFailureVar returns the name of the failure var.
func (es *ErrorSpec) AsFailureVar() string {
return "Failure" + strcase.ToCamel(es.failure)
}
// AsFailureString returns the OONI failure string.
func (es *ErrorSpec) AsFailureString() string {
return strcase.ToSnake(es.failure)
}
// NewSystemError constructs a new ErrorSpec representing a system
// error, i.e., an error returned by a system call.
func NewSystemError(errno, failure string) *ErrorSpec {
return &ErrorSpec{errno: errno, failure: failure}
}
// NewLibraryError constructs a new ErrorSpec representing a library
// error, i.e., an error returned by the Go standard library or by other
// dependecies written typicall in Go (e.g., quic-go).
func NewLibraryError(failure string) *ErrorSpec {
return &ErrorSpec{failure: failure}
}
// IsSystemError returns whether this ErrorSpec describes a system
// error, i.e., an error returned by a syscall.
func (es *ErrorSpec) IsSystemError() bool {
return es.errno != ""
}
// Specs contains all the error specs.
var Specs = []*ErrorSpec{
NewSystemError("ECANCELED", "operation_canceled"),
NewSystemError("ECONNREFUSED", "connection_refused"),
NewSystemError("ECONNRESET", "connection_reset"),
NewSystemError("EHOSTUNREACH", "host_unreachable"),
NewSystemError("ETIMEDOUT", "timed_out"),
NewSystemError("EAFNOSUPPORT", "address_family_not_supported"),
NewSystemError("EADDRINUSE", "address_in_use"),
NewSystemError("EADDRNOTAVAIL", "address_not_available"),
NewSystemError("EISCONN", "already_connected"),
NewSystemError("EFAULT", "bad_address"),
NewSystemError("EBADF", "bad_file_descriptor"),
NewSystemError("ECONNABORTED", "connection_aborted"),
NewSystemError("EALREADY", "connection_already_in_progress"),
NewSystemError("EDESTADDRREQ", "destination_address_required"),
NewSystemError("EINTR", "interrupted"),
NewSystemError("EINVAL", "invalid_argument"),
NewSystemError("EMSGSIZE", "message_size"),
NewSystemError("ENETDOWN", "network_down"),
NewSystemError("ENETRESET", "network_reset"),
NewSystemError("ENETUNREACH", "network_unreachable"),
NewSystemError("ENOBUFS", "no_buffer_space"),
NewSystemError("ENOPROTOOPT", "no_protocol_option"),
NewSystemError("ENOTSOCK", "not_a_socket"),
NewSystemError("ENOTCONN", "not_connected"),
NewSystemError("EWOULDBLOCK", "operation_would_block"),
NewSystemError("EACCES", "permission_denied"),
NewSystemError("EPROTONOSUPPORT", "protocol_not_supported"),
NewSystemError("EPROTOTYPE", "wrong_protocol_type"),
// Implementation note: we need to specify acronyms we
// want to be upper case in uppercase here. For example,
// we must write "DNS" rather than writing "dns".
NewLibraryError("DNS_bogon_error"),
NewLibraryError("DNS_NXDOMAIN_error"),
NewLibraryError("EOF_error"),
NewLibraryError("generic_timeout_error"),
NewLibraryError("QUIC_incompatible_version"),
NewLibraryError("SSL_failed_handshake"),
NewLibraryError("SSL_invalid_hostname"),
NewLibraryError("SSL_unknown_authority"),
NewLibraryError("SSL_invalid_certificate"),
NewLibraryError("JSON_parse_error"),
}
func fileCreate(filename string) *os.File {
filep, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
return filep
}
func fileWrite(filep *os.File, content string) {
if _, err := filep.WriteString(content); err != nil {
log.Fatal(err)
}
}
func fileClose(filep *os.File) {
if err := filep.Close(); err != nil {
log.Fatal(err)
}
}
func filePrintf(filep *os.File, format string, v ...interface{}) {
fileWrite(filep, fmt.Sprintf(format, v...))
}
func gofmt(filename string) {
cmd := execabs.Command("go", "fmt", filename)
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
func writeSystemSpecificFile(kind string) {
filename := "errno_" + kind + ".go"
filep := fileCreate(filename)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package errorsx\n\n")
filePrintf(filep, "import \"golang.org/x/sys/%s\"\n\n", kind)
fileWrite(filep, "const (\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
continue
}
filePrintf(filep, "\t%s = %s.%s\n",
spec.AsErrnoName(), kind, spec.AsErrnoName())
}
fileWrite(filep, ")\n\n")
fileClose(filep)
gofmt(filename)
}
func writeGenericFile() {
filename := "errno.go"
filep := fileCreate(filename)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package errorsx\n\n")
fileWrite(filep, "//go:generate go run ./internal/generrno/\n\n")
fileWrite(filep, "import (\n")
fileWrite(filep, "\t\"errors\"\n")
fileWrite(filep, "\t\"syscall\"\n")
fileWrite(filep, ")\n\n")
fileWrite(filep, "// This enumeration lists the failures defined at\n")
fileWrite(filep, "// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md\n")
fileWrite(filep, "const (\n")
fileWrite(filep, "//\n")
fileWrite(filep, "// System errors\n")
fileWrite(filep, "//\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
continue
}
filePrintf(filep, "\t%s = \"%s\"\n",
spec.AsFailureVar(),
spec.AsFailureString())
}
fileWrite(filep, "\n")
fileWrite(filep, "//\n")
fileWrite(filep, "// Library errors\n")
fileWrite(filep, "//\n")
for _, spec := range Specs {
if spec.IsSystemError() {
continue
}
filePrintf(filep, "\t%s = \"%s\"\n",
spec.AsFailureVar(),
spec.AsFailureString())
}
fileWrite(filep, ")\n\n")
fileWrite(filep, "// classifySyscallError converts a syscall error to the\n")
fileWrite(filep, "// proper OONI error. Returns the OONI error string\n")
fileWrite(filep, "// on success, an empty string otherwise.\n")
fileWrite(filep, "func classifySyscallError(err error) string {\n")
fileWrite(filep, "\t// filter out system errors: necessary to detect all windows errors\n")
fileWrite(filep, "\t// https://github.com/ooni/probe/issues/1526 describes the problem\n")
fileWrite(filep, "\t// of mapping localized windows errors.\n")
fileWrite(filep, "\tvar errno syscall.Errno\n")
fileWrite(filep, "\tif !errors.As(err, &errno) {\n")
fileWrite(filep, "\t\treturn \"\"\n")
fileWrite(filep, "\t}\n")
fileWrite(filep, "\tswitch errno {\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
continue
}
filePrintf(filep, "\tcase %s:\n", spec.AsErrnoName())
filePrintf(filep, "\t\treturn %s\n", spec.AsFailureVar())
}
fileWrite(filep, "\t}\n")
fileWrite(filep, "\treturn \"\"\n")
fileWrite(filep, "}\n\n")
fileClose(filep)
gofmt(filename)
}
func writeGenericTestFile() {
filename := "errno_test.go"
filep := fileCreate(filename)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package errorsx\n\n")
fileWrite(filep, "import (\n")
fileWrite(filep, "\t\"io\"\n")
fileWrite(filep, "\t\"syscall\"\n")
fileWrite(filep, "\t\"testing\"\n")
fileWrite(filep, ")\n\n")
fileWrite(filep, "func TestToSyscallErr(t *testing.T) {\n")
fileWrite(filep, "\tif v := classifySyscallError(io.EOF); v != \"\" {\n")
fileWrite(filep, "\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t}\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
continue
}
filePrintf(filep, "\tif v := classifySyscallError(%s); v != %s {\n",
spec.AsErrnoName(), spec.AsFailureVar())
filePrintf(filep, "\t\tt.Fatalf(\"expected '%%s', got '%%s'\", %s, v)\n",
spec.AsFailureVar())
fileWrite(filep, "\t}\n")
}
fileWrite(filep, "\tif v := classifySyscallError(syscall.Errno(0)); v != \"\" {\n")
fileWrite(filep, "\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t}\n")
fileWrite(filep, "}\n")
fileClose(filep)
gofmt(filename)
}
func main() {
writeSystemSpecificFile("unix")
writeSystemSpecificFile("windows")
writeGenericFile()
writeGenericTestFile()
}
+44
View File
@@ -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"
)
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"errors"
"strings"
"github.com/ooni/probe-cli/v3/internal/errorsx"
"github.com/ooni/probe-cli/v3/internal/netxlite/errorsx"
)
// This file contains weird stuff that we carried over from
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/errorsx"
"github.com/ooni/probe-cli/v3/internal/netxlite/errorsx"
)
func TestQuirkReduceErrors(t *testing.T) {