refactor(netxlite/errors): improve docs and format code (#481)

No real functional change. A few are needed and they will come
next. With this diff I just wanted to do cosmetic changes and
documentation changes, to ensure this package is okay.

See https://github.com/ooni/probe/issues/1591
This commit is contained in:
Simone Basso 2021-09-07 20:39:32 +02:00 committed by GitHub
parent 323266da83
commit a56b284b0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 346 additions and 135 deletions

View File

@ -18,6 +18,26 @@ import (
// ClassifyGenericError is the generic classifier mapping an error // ClassifyGenericError is the generic classifier mapping an error
// occurred during an operation to an OONI failure string. // 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 { func ClassifyGenericError(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.
@ -27,6 +47,9 @@ func ClassifyGenericError(err error) string {
return errwrapper.Error() // we've already wrapped it 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 != "" { if failure := classifySyscallError(err); failure != "" {
return failure return failure
} }
@ -34,6 +57,7 @@ func ClassifyGenericError(err error) string {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return FailureInterrupted return FailureInterrupted
} }
s := err.Error() s := err.Error()
if strings.HasSuffix(s, "operation was canceled") { if strings.HasSuffix(s, "operation was canceled") {
return FailureInterrupted return FailureInterrupted
@ -50,7 +74,6 @@ func ClassifyGenericError(err error) string {
if strings.HasSuffix(s, "i/o timeout") { if strings.HasSuffix(s, "i/o timeout") {
return FailureGenericTimeoutError return FailureGenericTimeoutError
} }
// TODO(kelmenhorst,bassosimone): this can probably be moved since it's TLS specific
if strings.HasSuffix(s, "TLS handshake timeout") { if strings.HasSuffix(s, "TLS handshake timeout") {
return FailureGenericTimeoutError return FailureGenericTimeoutError
} }
@ -60,11 +83,13 @@ func ClassifyGenericError(err error) string {
// that we return here is significantly more specific. // that we return here is significantly more specific.
return FailureDNSNXDOMAINError return FailureDNSNXDOMAINError
} }
formatted := fmt.Sprintf("unknown_failure: %s", s) formatted := fmt.Sprintf("unknown_failure: %s", s)
return scrubber.Scrub(formatted) // scrub IP addresses in the error return scrubber.Scrub(formatted) // scrub IP addresses in the error
} }
// TLS alert protocol as defined in RFC8446 // TLS alert protocol as defined in RFC8446. We need these definitions
// to figure out which error occurred during a QUIC handshake.
const ( const (
// Sender was unable to negotiate an acceptable set of security parameters given the options available. // Sender was unable to negotiate an acceptable set of security parameters given the options available.
quicTLSAlertHandshakeFailure = 40 quicTLSAlertHandshakeFailure = 40
@ -94,6 +119,11 @@ const (
quicTLSUnrecognizedName = 112 quicTLSUnrecognizedName = 112
) )
// 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 { func quicIsCertificateError(alert uint8) bool {
return (alert == quicTLSAlertBadCertificate || return (alert == quicTLSAlertBadCertificate ||
alert == quicTLSAlertUnsupportedCertificate || alert == quicTLSAlertUnsupportedCertificate ||
@ -104,17 +134,25 @@ func quicIsCertificateError(alert uint8) bool {
// ClassifyQUICHandshakeError maps an error occurred during the QUIC // ClassifyQUICHandshakeError maps an error occurred during the QUIC
// handshake to an OONI failure string. // 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 { func ClassifyQUICHandshakeError(err error) string {
var errwrapper *ErrWrapper var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) { if errors.As(err, &errwrapper) {
return errwrapper.Error() // we've already wrapped it return errwrapper.Error() // we've already wrapped it
} }
var versionNegotiation *quic.VersionNegotiationError var (
var statelessReset *quic.StatelessResetError versionNegotiation *quic.VersionNegotiationError
var handshakeTimeout *quic.HandshakeTimeoutError statelessReset *quic.StatelessResetError
var idleTimeout *quic.IdleTimeoutError handshakeTimeout *quic.HandshakeTimeoutError
var transportError *quic.TransportError idleTimeout *quic.IdleTimeoutError
transportError *quic.TransportError
)
if errors.As(err, &versionNegotiation) { if errors.As(err, &versionNegotiation) {
return FailureQUICIncompatibleVersion return FailureQUICIncompatibleVersion
@ -137,8 +175,9 @@ func ClassifyQUICHandshakeError(err error) string {
if quicIsCertificateError(errCode) { if quicIsCertificateError(errCode) {
return FailureSSLInvalidCertificate return FailureSSLInvalidCertificate
} }
// TLSAlertDecryptError and TLSAlertHandshakeFailure are summarized to a FailureSSLHandshake error because both // TLSAlertDecryptError and TLSAlertHandshakeFailure are summarized to a
// alerts are caused by a failed or corrupted parameter negotiation during the TLS handshake. // FailureSSLHandshake error because both alerts are caused by a failed or
// corrupted parameter negotiation during the TLS handshake.
if errCode == quicTLSAlertDecryptError || errCode == quicTLSAlertHandshakeFailure { if errCode == quicTLSAlertDecryptError || errCode == quicTLSAlertHandshakeFailure {
return FailureSSLFailedHandshake return FailureSSLFailedHandshake
} }
@ -152,13 +191,18 @@ func ClassifyQUICHandshakeError(err error) string {
return ClassifyGenericError(err) return ClassifyGenericError(err)
} }
// ErrDNSBogon indicates that we found a bogon address. This is the // ErrDNSBogon indicates that we found a bogon address. Code that
// correct value with which to initialize MeasurementRoot.ErrDNSBogon // filters for DNS bogons MUST use this error.
// to tell this library to return an error when a bogon is found.
var ErrDNSBogon = errors.New("dns: detected bogon address") var ErrDNSBogon = errors.New("dns: detected bogon address")
// ClassifyResolverError maps an error occurred during a domain name // ClassifyResolverError maps an error occurred during a domain name
// resolution to the corresponding OONI failure string. // 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 { func ClassifyResolverError(err error) string {
var errwrapper *ErrWrapper var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) { if errors.As(err, &errwrapper) {
@ -172,6 +216,12 @@ func ClassifyResolverError(err error) string {
// ClassifyTLSHandshakeError maps an error occurred during the TLS // ClassifyTLSHandshakeError maps an error occurred during the TLS
// handshake to an OONI failure string. // 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 { func ClassifyTLSHandshakeError(err error) string {
var errwrapper *ErrWrapper var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) { if errors.As(err, &errwrapper) {

View File

@ -22,52 +22,62 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal("did not classify existing ErrWrapper correctly") t.Fatal("did not classify existing ErrWrapper correctly")
} }
}) })
t.Run("for already wrapped error", func(t *testing.T) { t.Run("for already wrapped error", func(t *testing.T) {
err := io.EOF err := io.EOF
if ClassifyGenericError(err) != FailureEOFError { if ClassifyGenericError(err) != FailureEOFError {
t.Fatal("unexpected result") t.Fatal("unexpected result")
} }
}) })
t.Run("for context.Canceled", func(t *testing.T) { t.Run("for context.Canceled", func(t *testing.T) {
if ClassifyGenericError(context.Canceled) != FailureInterrupted { if ClassifyGenericError(context.Canceled) != FailureInterrupted {
t.Fatal("unexpected result") 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 ClassifyGenericError(errors.New("operation was canceled")) != FailureInterrupted { if ClassifyGenericError(errors.New("operation was canceled")) != FailureInterrupted {
t.Fatal("unexpected result") t.Fatal("unexpected result")
} }
}) })
t.Run("for EOF", func(t *testing.T) { t.Run("for EOF", func(t *testing.T) {
if ClassifyGenericError(io.EOF) != FailureEOFError { if ClassifyGenericError(io.EOF) != FailureEOFError {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for canceled", func(t *testing.T) { t.Run("for canceled", func(t *testing.T) {
if ClassifyGenericError(syscall.ECANCELED) != FailureOperationCanceled { if ClassifyGenericError(syscall.ECANCELED) != FailureOperationCanceled {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for connection_refused", func(t *testing.T) { t.Run("for connection_refused", func(t *testing.T) {
if ClassifyGenericError(syscall.ECONNREFUSED) != FailureConnectionRefused { if ClassifyGenericError(syscall.ECONNREFUSED) != FailureConnectionRefused {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for connection_reset", func(t *testing.T) { t.Run("for connection_reset", func(t *testing.T) {
if ClassifyGenericError(syscall.ECONNRESET) != FailureConnectionReset { if ClassifyGenericError(syscall.ECONNRESET) != FailureConnectionReset {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for host_unreachable", func(t *testing.T) { t.Run("for host_unreachable", func(t *testing.T) {
if ClassifyGenericError(syscall.EHOSTUNREACH) != FailureHostUnreachable { if ClassifyGenericError(syscall.EHOSTUNREACH) != FailureHostUnreachable {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for system timeout", func(t *testing.T) { t.Run("for system timeout", func(t *testing.T) {
if ClassifyGenericError(syscall.ETIMEDOUT) != FailureTimedOut { if ClassifyGenericError(syscall.ETIMEDOUT) != FailureTimedOut {
t.Fatal("unexpected results") 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()
@ -76,11 +86,13 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for stun's transaction is timed out", func(t *testing.T) { t.Run("for stun's transaction is timed out", func(t *testing.T) {
if ClassifyGenericError(stun.ErrTransactionTimeOut) != FailureGenericTimeoutError { if ClassifyGenericError(stun.ErrTransactionTimeOut) != FailureGenericTimeoutError {
t.Fatal("unexpected results") 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
@ -95,12 +107,14 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for TLS handshake timeout error", func(t *testing.T) { t.Run("for TLS handshake timeout error", func(t *testing.T) {
err := errors.New("net/http: TLS handshake timeout") err := errors.New("net/http: TLS handshake timeout")
if ClassifyGenericError(err) != FailureGenericTimeoutError { if ClassifyGenericError(err) != FailureGenericTimeoutError {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for no such host", func(t *testing.T) { t.Run("for no such host", func(t *testing.T) {
if ClassifyGenericError(&net.DNSError{ if ClassifyGenericError(&net.DNSError{
Err: "no such host", Err: "no such host",
@ -108,6 +122,7 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for errors including IPv4 address", func(t *testing.T) { 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") 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" expected := "unknown_failure: read tcp [scrubbed]->[scrubbed]: use of closed network connection"
@ -116,6 +131,7 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal(cmp.Diff(expected, out)) t.Fatal(cmp.Diff(expected, out))
} }
}) })
t.Run("for errors including IPv6 address", func(t *testing.T) { 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") 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" expected := "unknown_failure: read tcp [scrubbed]->[scrubbed]: use of closed network connection"
@ -124,6 +140,7 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal(cmp.Diff(expected, out)) t.Fatal(cmp.Diff(expected, out))
} }
}) })
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
@ -152,55 +169,65 @@ func TestClassifyQUICHandshakeError(t *testing.T) {
t.Fatal("did not classify existing ErrWrapper correctly") t.Fatal("did not classify existing ErrWrapper correctly")
} }
}) })
t.Run("for connection_reset", func(t *testing.T) { t.Run("for connection_reset", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.StatelessResetError{}) != FailureConnectionReset { if ClassifyQUICHandshakeError(&quic.StatelessResetError{}) != FailureConnectionReset {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for incompatible quic version", func(t *testing.T) { t.Run("for incompatible quic version", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.VersionNegotiationError{}) != FailureQUICIncompatibleVersion { if ClassifyQUICHandshakeError(&quic.VersionNegotiationError{}) != FailureQUICIncompatibleVersion {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for quic connection refused", func(t *testing.T) { t.Run("for quic connection refused", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: quic.ConnectionRefused}) != FailureConnectionRefused { if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: quic.ConnectionRefused}) != FailureConnectionRefused {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for quic handshake timeout", func(t *testing.T) { t.Run("for quic handshake timeout", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.HandshakeTimeoutError{}) != FailureGenericTimeoutError { if ClassifyQUICHandshakeError(&quic.HandshakeTimeoutError{}) != FailureGenericTimeoutError {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for QUIC idle connection timeout", func(t *testing.T) { t.Run("for QUIC idle connection timeout", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.IdleTimeoutError{}) != FailureGenericTimeoutError { if ClassifyQUICHandshakeError(&quic.IdleTimeoutError{}) != FailureGenericTimeoutError {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for QUIC CRYPTO Handshake", func(t *testing.T) { t.Run("for QUIC CRYPTO Handshake", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertHandshakeFailure var err quic.TransportErrorCode = quicTLSAlertHandshakeFailure
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLFailedHandshake { if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLFailedHandshake {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for QUIC CRYPTO Invalid Certificate", func(t *testing.T) { t.Run("for QUIC CRYPTO Invalid Certificate", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertBadCertificate var err quic.TransportErrorCode = quicTLSAlertBadCertificate
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidCertificate { if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidCertificate {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for QUIC CRYPTO Unknown CA", func(t *testing.T) { t.Run("for QUIC CRYPTO Unknown CA", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertUnknownCA var err quic.TransportErrorCode = quicTLSAlertUnknownCA
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLUnknownAuthority { if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLUnknownAuthority {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for QUIC CRYPTO Bad Hostname", func(t *testing.T) { t.Run("for QUIC CRYPTO Bad Hostname", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSUnrecognizedName var err quic.TransportErrorCode = quicTLSUnrecognizedName
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidHostname { if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidHostname {
t.Fatal("unexpected results") t.Fatal("unexpected results")
} }
}) })
t.Run("for another kind of error", func(t *testing.T) { t.Run("for another kind of error", func(t *testing.T) {
if ClassifyQUICHandshakeError(io.EOF) != FailureEOFError { if ClassifyQUICHandshakeError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result") t.Fatal("unexpected result")
@ -215,11 +242,13 @@ func TestClassifyResolverError(t *testing.T) {
t.Fatal("did not classify existing ErrWrapper correctly") t.Fatal("did not classify existing ErrWrapper correctly")
} }
}) })
t.Run("for ErrDNSBogon", func(t *testing.T) { t.Run("for ErrDNSBogon", func(t *testing.T) {
if ClassifyResolverError(ErrDNSBogon) != FailureDNSBogonError { if ClassifyResolverError(ErrDNSBogon) != FailureDNSBogonError {
t.Fatal("unexpected result") t.Fatal("unexpected result")
} }
}) })
t.Run("for another kind of error", func(t *testing.T) { t.Run("for another kind of error", func(t *testing.T) {
if ClassifyResolverError(io.EOF) != FailureEOFError { if ClassifyResolverError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result") t.Fatal("unexpected result")
@ -234,24 +263,28 @@ func TestClassifyTLSHandshakeError(t *testing.T) {
t.Fatal("did not classify existing ErrWrapper correctly") t.Fatal("did not classify existing ErrWrapper correctly")
} }
}) })
t.Run("for x509.HostnameError", func(t *testing.T) { t.Run("for x509.HostnameError", func(t *testing.T) {
var err x509.HostnameError var err x509.HostnameError
if ClassifyTLSHandshakeError(err) != FailureSSLInvalidHostname { if ClassifyTLSHandshakeError(err) != FailureSSLInvalidHostname {
t.Fatal("unexpected result") t.Fatal("unexpected result")
} }
}) })
t.Run("for x509.UnknownAuthorityError", func(t *testing.T) { t.Run("for x509.UnknownAuthorityError", func(t *testing.T) {
var err x509.UnknownAuthorityError var err x509.UnknownAuthorityError
if ClassifyTLSHandshakeError(err) != FailureSSLUnknownAuthority { if ClassifyTLSHandshakeError(err) != FailureSSLUnknownAuthority {
t.Fatal("unexpected result") t.Fatal("unexpected result")
} }
}) })
t.Run("for x509.CertificateInvalidError", func(t *testing.T) { t.Run("for x509.CertificateInvalidError", func(t *testing.T) {
var err x509.CertificateInvalidError var err x509.CertificateInvalidError
if ClassifyTLSHandshakeError(err) != FailureSSLInvalidCertificate { if ClassifyTLSHandshakeError(err) != FailureSSLInvalidCertificate {
t.Fatal("unexpected result") t.Fatal("unexpected result")
} }
}) })
t.Run("for another kind of error", func(t *testing.T) { t.Run("for another kind of error", func(t *testing.T) {
if ClassifyTLSHandshakeError(io.EOF) != FailureEOFError { if ClassifyTLSHandshakeError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result") t.Fatal("unexpected result")

View File

@ -1,2 +1,20 @@
// Package errorsx contains code to classify errors. // Package errorsx contains code to classify errors.
//
// We define the ErrWrapper type, that should wrap any error
// and map it to the corresponding OONI failure.
//
// See https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md
// for a list of OONI failure strings.
//
// We define ClassifyXXX functions that map an `error` type to
// the corresponding OONI failure.
//
// When we cannot map an error to an OONI failure we return
// an "unknown_failure: XXX" string where the XXX part has
// been scrubbed so to remove any network endpoints.
//
// The general approach we have been following for this
// package has been to return the same strings that we used
// with the previous measurement engine, Measurement Kit
// available at https://github.com/measurement-kit/measurement-kit.
package errorsx package errorsx

View File

@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT. // Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.462721 +0200 CEST m=+0.105415376 // Generated: 2021-09-07 20:26:06.502417 +0200 CEST m=+0.133209876
package errorsx package errorsx

View File

@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT. // Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.510432 +0200 CEST m=+0.153127376 // Generated: 2021-09-07 20:26:06.547786 +0200 CEST m=+0.178579584
package errorsx package errorsx
@ -9,95 +9,184 @@ import (
"testing" "testing"
) )
func TestToSyscallErr(t *testing.T) { func TestClassifySyscallError(t *testing.T) {
if v := classifySyscallError(io.EOF); v != "" { t.Run("for a non-syscall error", func(t *testing.T) {
t.Fatalf("expected empty string, got '%s'", v) 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.Run("for ECANCELED", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureConnectionRefused, v) if v := classifySyscallError(ECANCELED); v != FailureOperationCanceled {
} t.Fatalf("expected '%s', got '%s'", FailureOperationCanceled, v)
if v := classifySyscallError(ECONNRESET); v != FailureConnectionReset { }
t.Fatalf("expected '%s', got '%s'", FailureConnectionReset, v) })
}
if v := classifySyscallError(EHOSTUNREACH); v != FailureHostUnreachable { t.Run("for ECONNREFUSED", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureHostUnreachable, v) if v := classifySyscallError(ECONNREFUSED); v != FailureConnectionRefused {
} t.Fatalf("expected '%s', got '%s'", FailureConnectionRefused, v)
if v := classifySyscallError(ETIMEDOUT); v != FailureTimedOut { }
t.Fatalf("expected '%s', got '%s'", FailureTimedOut, v) })
}
if v := classifySyscallError(EAFNOSUPPORT); v != FailureAddressFamilyNotSupported { t.Run("for ECONNRESET", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureAddressFamilyNotSupported, v) if v := classifySyscallError(ECONNRESET); v != FailureConnectionReset {
} t.Fatalf("expected '%s', got '%s'", FailureConnectionReset, v)
if v := classifySyscallError(EADDRINUSE); v != FailureAddressInUse { }
t.Fatalf("expected '%s', got '%s'", FailureAddressInUse, v) })
}
if v := classifySyscallError(EADDRNOTAVAIL); v != FailureAddressNotAvailable { t.Run("for EHOSTUNREACH", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureAddressNotAvailable, v) if v := classifySyscallError(EHOSTUNREACH); v != FailureHostUnreachable {
} t.Fatalf("expected '%s', got '%s'", FailureHostUnreachable, v)
if v := classifySyscallError(EISCONN); v != FailureAlreadyConnected { }
t.Fatalf("expected '%s', got '%s'", FailureAlreadyConnected, v) })
}
if v := classifySyscallError(EFAULT); v != FailureBadAddress { t.Run("for ETIMEDOUT", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureBadAddress, v) if v := classifySyscallError(ETIMEDOUT); v != FailureTimedOut {
} t.Fatalf("expected '%s', got '%s'", FailureTimedOut, v)
if v := classifySyscallError(EBADF); v != FailureBadFileDescriptor { }
t.Fatalf("expected '%s', got '%s'", FailureBadFileDescriptor, v) })
}
if v := classifySyscallError(ECONNABORTED); v != FailureConnectionAborted { t.Run("for EAFNOSUPPORT", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureConnectionAborted, v) if v := classifySyscallError(EAFNOSUPPORT); v != FailureAddressFamilyNotSupported {
} t.Fatalf("expected '%s', got '%s'", FailureAddressFamilyNotSupported, v)
if v := classifySyscallError(EALREADY); v != FailureConnectionAlreadyInProgress { }
t.Fatalf("expected '%s', got '%s'", FailureConnectionAlreadyInProgress, v) })
}
if v := classifySyscallError(EDESTADDRREQ); v != FailureDestinationAddressRequired { t.Run("for EADDRINUSE", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureDestinationAddressRequired, v) if v := classifySyscallError(EADDRINUSE); v != FailureAddressInUse {
} t.Fatalf("expected '%s', got '%s'", FailureAddressInUse, v)
if v := classifySyscallError(EINTR); v != FailureInterrupted { }
t.Fatalf("expected '%s', got '%s'", FailureInterrupted, v) })
}
if v := classifySyscallError(EINVAL); v != FailureInvalidArgument { t.Run("for EADDRNOTAVAIL", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureInvalidArgument, v) if v := classifySyscallError(EADDRNOTAVAIL); v != FailureAddressNotAvailable {
} t.Fatalf("expected '%s', got '%s'", FailureAddressNotAvailable, v)
if v := classifySyscallError(EMSGSIZE); v != FailureMessageSize { }
t.Fatalf("expected '%s', got '%s'", FailureMessageSize, v) })
}
if v := classifySyscallError(ENETDOWN); v != FailureNetworkDown { t.Run("for EISCONN", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureNetworkDown, v) if v := classifySyscallError(EISCONN); v != FailureAlreadyConnected {
} t.Fatalf("expected '%s', got '%s'", FailureAlreadyConnected, v)
if v := classifySyscallError(ENETRESET); v != FailureNetworkReset { }
t.Fatalf("expected '%s', got '%s'", FailureNetworkReset, v) })
}
if v := classifySyscallError(ENETUNREACH); v != FailureNetworkUnreachable { t.Run("for EFAULT", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureNetworkUnreachable, v) if v := classifySyscallError(EFAULT); v != FailureBadAddress {
} t.Fatalf("expected '%s', got '%s'", FailureBadAddress, v)
if v := classifySyscallError(ENOBUFS); v != FailureNoBufferSpace { }
t.Fatalf("expected '%s', got '%s'", FailureNoBufferSpace, v) })
}
if v := classifySyscallError(ENOPROTOOPT); v != FailureNoProtocolOption { t.Run("for EBADF", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureNoProtocolOption, v) if v := classifySyscallError(EBADF); v != FailureBadFileDescriptor {
} t.Fatalf("expected '%s', got '%s'", FailureBadFileDescriptor, v)
if v := classifySyscallError(ENOTSOCK); v != FailureNotASocket { }
t.Fatalf("expected '%s', got '%s'", FailureNotASocket, v) })
}
if v := classifySyscallError(ENOTCONN); v != FailureNotConnected { t.Run("for ECONNABORTED", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureNotConnected, v) if v := classifySyscallError(ECONNABORTED); v != FailureConnectionAborted {
} t.Fatalf("expected '%s', got '%s'", FailureConnectionAborted, v)
if v := classifySyscallError(EWOULDBLOCK); v != FailureOperationWouldBlock { }
t.Fatalf("expected '%s', got '%s'", FailureOperationWouldBlock, v) })
}
if v := classifySyscallError(EACCES); v != FailurePermissionDenied { t.Run("for EALREADY", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailurePermissionDenied, v) if v := classifySyscallError(EALREADY); v != FailureConnectionAlreadyInProgress {
} t.Fatalf("expected '%s', got '%s'", FailureConnectionAlreadyInProgress, v)
if v := classifySyscallError(EPROTONOSUPPORT); v != FailureProtocolNotSupported { }
t.Fatalf("expected '%s', got '%s'", FailureProtocolNotSupported, v) })
}
if v := classifySyscallError(EPROTOTYPE); v != FailureWrongProtocolType { t.Run("for EDESTADDRREQ", func(t *testing.T) {
t.Fatalf("expected '%s', got '%s'", FailureWrongProtocolType, v) if v := classifySyscallError(EDESTADDRREQ); v != FailureDestinationAddressRequired {
} t.Fatalf("expected '%s', got '%s'", FailureDestinationAddressRequired, v)
if v := classifySyscallError(syscall.Errno(0)); v != "" { }
t.Fatalf("expected empty string, got '%s'", v) })
}
t.Run("for EINTR", func(t *testing.T) {
if v := classifySyscallError(EINTR); v != FailureInterrupted {
t.Fatalf("expected '%s', got '%s'", FailureInterrupted, v)
}
})
t.Run("for EINVAL", func(t *testing.T) {
if v := classifySyscallError(EINVAL); v != FailureInvalidArgument {
t.Fatalf("expected '%s', got '%s'", FailureInvalidArgument, v)
}
})
t.Run("for EMSGSIZE", func(t *testing.T) {
if v := classifySyscallError(EMSGSIZE); v != FailureMessageSize {
t.Fatalf("expected '%s', got '%s'", FailureMessageSize, v)
}
})
t.Run("for ENETDOWN", func(t *testing.T) {
if v := classifySyscallError(ENETDOWN); v != FailureNetworkDown {
t.Fatalf("expected '%s', got '%s'", FailureNetworkDown, v)
}
})
t.Run("for ENETRESET", func(t *testing.T) {
if v := classifySyscallError(ENETRESET); v != FailureNetworkReset {
t.Fatalf("expected '%s', got '%s'", FailureNetworkReset, v)
}
})
t.Run("for ENETUNREACH", func(t *testing.T) {
if v := classifySyscallError(ENETUNREACH); v != FailureNetworkUnreachable {
t.Fatalf("expected '%s', got '%s'", FailureNetworkUnreachable, v)
}
})
t.Run("for ENOBUFS", func(t *testing.T) {
if v := classifySyscallError(ENOBUFS); v != FailureNoBufferSpace {
t.Fatalf("expected '%s', got '%s'", FailureNoBufferSpace, v)
}
})
t.Run("for ENOPROTOOPT", func(t *testing.T) {
if v := classifySyscallError(ENOPROTOOPT); v != FailureNoProtocolOption {
t.Fatalf("expected '%s', got '%s'", FailureNoProtocolOption, v)
}
})
t.Run("for ENOTSOCK", func(t *testing.T) {
if v := classifySyscallError(ENOTSOCK); v != FailureNotASocket {
t.Fatalf("expected '%s', got '%s'", FailureNotASocket, v)
}
})
t.Run("for ENOTCONN", func(t *testing.T) {
if v := classifySyscallError(ENOTCONN); v != FailureNotConnected {
t.Fatalf("expected '%s', got '%s'", FailureNotConnected, v)
}
})
t.Run("for EWOULDBLOCK", func(t *testing.T) {
if v := classifySyscallError(EWOULDBLOCK); v != FailureOperationWouldBlock {
t.Fatalf("expected '%s', got '%s'", FailureOperationWouldBlock, v)
}
})
t.Run("for EACCES", func(t *testing.T) {
if v := classifySyscallError(EACCES); v != FailurePermissionDenied {
t.Fatalf("expected '%s', got '%s'", FailurePermissionDenied, v)
}
})
t.Run("for EPROTONOSUPPORT", func(t *testing.T) {
if v := classifySyscallError(EPROTONOSUPPORT); v != FailureProtocolNotSupported {
t.Fatalf("expected '%s', got '%s'", FailureProtocolNotSupported, v)
}
})
t.Run("for EPROTOTYPE", func(t *testing.T) {
if v := classifySyscallError(EPROTOTYPE); v != FailureWrongProtocolType {
t.Fatalf("expected '%s', got '%s'", FailureWrongProtocolType, v)
}
})
t.Run("for the zero errno value", func(t *testing.T) {
if v := classifySyscallError(syscall.Errno(0)); v != "" {
t.Fatalf("expected empty string, got '%s'", v)
}
})
} }

View File

@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT. // Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.35751 +0200 CEST m=+0.000202959 // Generated: 2021-09-07 20:26:06.370246 +0200 CEST m=+0.001036417
package errorsx package errorsx

View File

@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT. // Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.436584 +0200 CEST m=+0.079277834 // Generated: 2021-09-07 20:26:06.478577 +0200 CEST m=+0.109369751
package errorsx package errorsx

View File

@ -2,17 +2,28 @@ package errorsx
// ErrWrapper is our error wrapper for Go errors. The key objective of // ErrWrapper is our error wrapper for Go errors. The key objective of
// this structure is to properly set Failure, which is also returned by // this structure is to properly set Failure, which is also returned by
// the Error() method, so be one of the OONI defined strings. // the Error() method, to be one of the OONI failure strings.
//
// OONI failure strings are defined in the github.com/ooni/spec repo
// at https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md.
type ErrWrapper struct { type ErrWrapper struct {
// Failure is the OONI failure string. The failure strings are // Failure is the OONI failure string. The failure strings are
// loosely backward compatible with Measurement Kit. // loosely backward compatible with Measurement Kit.
// //
// This is either one of the FailureXXX strings or any other // This is either one of the FailureXXX strings or any other
// string like `unknown_failure ...`. The latter represents an // string like `unknown_failure: ...`. The latter represents an
// error that we have not yet mapped to a failure. // error that we have not yet mapped to a failure.
Failure string Failure string
// Operation is the operation that failed. If possible, it // Operation is the operation that failed.
//
// New code will always nest ErrWrapper and you need to
// walk the chain to find what happened.
//
// The following comment describes the DEPRECATED
// legacy behavior implements by internal/engine/legacy/errorsx:
//
// If possible, the Operation string
// SHOULD be a _major_ operation. Major operations are: // SHOULD be a _major_ operation. Major operations are:
// //
// - ResolveOperation: resolving a domain name failed // - ResolveOperation: resolving a domain name failed

View File

@ -6,19 +6,21 @@ import (
"testing" "testing"
) )
func TestErrWrapperError(t *testing.T) { func TestErrWrapper(t *testing.T) {
err := &ErrWrapper{Failure: FailureDNSNXDOMAINError} t.Run("Error", func(t *testing.T) {
if err.Error() != FailureDNSNXDOMAINError { err := &ErrWrapper{Failure: FailureDNSNXDOMAINError}
t.Fatal("invalid return value") if err.Error() != FailureDNSNXDOMAINError {
} t.Fatal("invalid return value")
} }
})
func TestErrWrapperUnwrap(t *testing.T) { t.Run("Unwrap", func(t *testing.T) {
err := &ErrWrapper{ err := &ErrWrapper{
Failure: FailureEOFError, Failure: FailureEOFError,
WrappedErr: io.EOF, WrappedErr: io.EOF,
} }
if !errors.Is(err, io.EOF) { if !errors.Is(err, io.EOF) {
t.Fatal("cannot unwrap error") t.Fatal("cannot unwrap error")
} }
})
} }

View File

@ -234,25 +234,32 @@ func writeGenericTestFile() {
fileWrite(filep, "\t\"testing\"\n") fileWrite(filep, "\t\"testing\"\n")
fileWrite(filep, ")\n\n") fileWrite(filep, ")\n\n")
fileWrite(filep, "func TestToSyscallErr(t *testing.T) {\n") fileWrite(filep, "func TestClassifySyscallError(t *testing.T) {\n")
fileWrite(filep, "\tif v := classifySyscallError(io.EOF); v != \"\" {\n") fileWrite(filep, "\tt.Run(\"for a non-syscall error\", func (t *testing.T) {\n")
fileWrite(filep, "\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n") fileWrite(filep, "\t\tif v := classifySyscallError(io.EOF); v != \"\" {\n")
fileWrite(filep, "\t}\n") fileWrite(filep, "\t\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t\t}\n")
fileWrite(filep, "\t})\n\n")
for _, spec := range Specs { for _, spec := range Specs {
if !spec.IsSystemError() { if !spec.IsSystemError() {
continue continue
} }
filePrintf(filep, "\tif v := classifySyscallError(%s); v != %s {\n", filePrintf(filep, "\tt.Run(\"for %s\", func (t *testing.T) {\n",
spec.AsErrnoName())
filePrintf(filep, "\t\tif v := classifySyscallError(%s); v != %s {\n",
spec.AsErrnoName(), spec.AsFailureVar()) spec.AsErrnoName(), spec.AsFailureVar())
filePrintf(filep, "\t\tt.Fatalf(\"expected '%%s', got '%%s'\", %s, v)\n", filePrintf(filep, "\t\t\tt.Fatalf(\"expected '%%s', got '%%s'\", %s, v)\n",
spec.AsFailureVar()) spec.AsFailureVar())
fileWrite(filep, "\t}\n") fileWrite(filep, "\t\t}\n")
fileWrite(filep, "\t})\n\n")
} }
fileWrite(filep, "\tif v := classifySyscallError(syscall.Errno(0)); v != \"\" {\n") fileWrite(filep, "\tt.Run(\"for the zero errno value\", func (t *testing.T) {\n")
fileWrite(filep, "\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n") fileWrite(filep, "\t\tif v := classifySyscallError(syscall.Errno(0)); v != \"\" {\n")
fileWrite(filep, "\t}\n") fileWrite(filep, "\t\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t\t}\n")
fileWrite(filep, "\t})\n")
fileWrite(filep, "}\n") fileWrite(filep, "}\n")
fileClose(filep) fileClose(filep)

View File

@ -1,6 +1,7 @@
package errorsx package errorsx
// Operations that we measure. // Operations that we measure. They are the possibly values of
// the ErrWrapper.Operation field.
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"