refactor(errorsx): start hiding private details and moving around stuff (#424)
* refactor(errorsx): start hiding private details and moving around stuff Part of https://github.com/ooni/probe/issues/1505 * fix: remove now-addressed todo comments
This commit is contained in:
parent
ceb2aa8a8d
commit
17bfb052c5
|
@ -11,12 +11,12 @@ type Dialer interface {
|
||||||
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorWrapperDialer is a dialer that performs err wrapping.
|
// ErrorWrapperDialer is a dialer that performs error wrapping.
|
||||||
type ErrorWrapperDialer struct {
|
type ErrorWrapperDialer struct {
|
||||||
Dialer
|
Dialer
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements Dialer.DialContext
|
// DialContext implements Dialer.DialContext.
|
||||||
func (d *ErrorWrapperDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
func (d *ErrorWrapperDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
conn, err := d.Dialer.DialContext(ctx, network, address)
|
conn, err := d.Dialer.DialContext(ctx, network, address)
|
||||||
err = SafeErrWrapperBuilder{
|
err = SafeErrWrapperBuilder{
|
||||||
|
@ -34,7 +34,7 @@ type errorWrapperConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read implements net.Conn.Read
|
// Read implements net.Conn.Read.
|
||||||
func (c *errorWrapperConn) Read(b []byte) (n int, err error) {
|
func (c *errorWrapperConn) Read(b []byte) (n int, err error) {
|
||||||
n, err = c.Conn.Read(b)
|
n, err = c.Conn.Read(b)
|
||||||
err = SafeErrWrapperBuilder{
|
err = SafeErrWrapperBuilder{
|
||||||
|
@ -44,7 +44,7 @@ func (c *errorWrapperConn) Read(b []byte) (n int, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write implements net.Conn.Write
|
// Write implements net.Conn.Write.
|
||||||
func (c *errorWrapperConn) Write(b []byte) (n int, err error) {
|
func (c *errorWrapperConn) Write(b []byte) (n int, err error) {
|
||||||
n, err = c.Conn.Write(b)
|
n, err = c.Conn.Write(b)
|
||||||
err = SafeErrWrapperBuilder{
|
err = SafeErrWrapperBuilder{
|
||||||
|
@ -54,7 +54,7 @@ func (c *errorWrapperConn) Write(b []byte) (n int, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close implements net.Conn.Close
|
// Close implements net.Conn.Close.
|
||||||
func (c *errorWrapperConn) Close() (err error) {
|
func (c *errorWrapperConn) Close() (err error) {
|
||||||
err = c.Conn.Close()
|
err = c.Conn.Close()
|
||||||
err = SafeErrWrapperBuilder{
|
err = SafeErrWrapperBuilder{
|
||||||
|
|
|
@ -1,142 +1,16 @@
|
||||||
// Package errorsx contains error extensions.
|
// Package errorsx contains error extensions.
|
||||||
package errorsx
|
package errorsx
|
||||||
|
|
||||||
// TODO: eventually we want to re-structure the error classification code by clearly separating the layers where the error occur:
|
|
||||||
//
|
|
||||||
// - errno.go and errno_test.go: contain only the errno classifier (for system errors)
|
|
||||||
// - qtls.go and qtls_test.go: contain qtls dialers, handshaker, classifier
|
|
||||||
// - tls.go and tls_test.go: contain tls dialers, handshaker, classifier
|
|
||||||
// - resolver.go and resolver_test.go: contain dialers and classifier for resolving
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/scrubber"
|
"github.com/ooni/probe-cli/v3/internal/scrubber"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// FailureConnectionRefused means ECONNREFUSED.
|
|
||||||
FailureConnectionRefused = "connection_refused"
|
|
||||||
|
|
||||||
// FailureConnectionReset means ECONNRESET.
|
|
||||||
FailureConnectionReset = "connection_reset"
|
|
||||||
|
|
||||||
// FailureDNSBogonError means we detected bogon in DNS reply.
|
|
||||||
FailureDNSBogonError = "dns_bogon_error"
|
|
||||||
|
|
||||||
// FailureDNSNXDOMAINError means we got NXDOMAIN in DNS reply.
|
|
||||||
FailureDNSNXDOMAINError = "dns_nxdomain_error"
|
|
||||||
|
|
||||||
// FailureEOFError means we got unexpected EOF on connection.
|
|
||||||
FailureEOFError = "eof_error"
|
|
||||||
|
|
||||||
// FailureGenericTimeoutError means we got some timer has expired.
|
|
||||||
FailureGenericTimeoutError = "generic_timeout_error"
|
|
||||||
|
|
||||||
// FailureHostUnreachable means that there is "no route to host".
|
|
||||||
FailureHostUnreachable = "host_unreachable"
|
|
||||||
|
|
||||||
// FailureInterrupted means that the user interrupted us.
|
|
||||||
FailureInterrupted = "interrupted"
|
|
||||||
|
|
||||||
// FailureNoCompatibleQUICVersion means that the server does not support the proposed QUIC version
|
|
||||||
FailureNoCompatibleQUICVersion = "quic_incompatible_version"
|
|
||||||
|
|
||||||
// FailureSSLHandshake means that the negotiation of cryptographic parameters failed
|
|
||||||
FailureSSLHandshake = "ssl_failed_handshake"
|
|
||||||
|
|
||||||
// FailureSSLInvalidHostname means we got certificate is not valid for SNI.
|
|
||||||
FailureSSLInvalidHostname = "ssl_invalid_hostname"
|
|
||||||
|
|
||||||
// FailureSSLUnknownAuthority means we cannot find CA validating certificate.
|
|
||||||
FailureSSLUnknownAuthority = "ssl_unknown_authority"
|
|
||||||
|
|
||||||
// FailureSSLInvalidCertificate means certificate experired or other
|
|
||||||
// sort of errors causing it to be invalid.
|
|
||||||
FailureSSLInvalidCertificate = "ssl_invalid_certificate"
|
|
||||||
|
|
||||||
// FailureJSONParseError indicates that we couldn't parse a JSON
|
|
||||||
FailureJSONParseError = "json_parse_error"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TLS alert protocol as defined in RFC8446
|
|
||||||
const (
|
|
||||||
// Sender was unable to negotiate an acceptable set of security parameters given the options available.
|
|
||||||
TLSAlertHandshakeFailure = 40
|
|
||||||
|
|
||||||
// Certificate was corrupt, contained signatures that did not verify correctly, etc.
|
|
||||||
TLSAlertBadCertificate = 42
|
|
||||||
|
|
||||||
// Certificate was of an unsupported type.
|
|
||||||
TLSAlertUnsupportedCertificate = 43
|
|
||||||
|
|
||||||
// Certificate was revoked by its signer.
|
|
||||||
TLSAlertCertificateRevoked = 44
|
|
||||||
|
|
||||||
// Certificate has expired or is not currently valid.
|
|
||||||
TLSAlertCertificateExpired = 45
|
|
||||||
|
|
||||||
// Some unspecified issue arose in processing the certificate, rendering it unacceptable.
|
|
||||||
TLSAlertCertificateUnknown = 46
|
|
||||||
|
|
||||||
// Certificate was not accepted because the CA certificate could not be located or could not be matched with a known trust anchor.
|
|
||||||
TLSAlertUnknownCA = 48
|
|
||||||
|
|
||||||
// Handshake (not record layer) cryptographic operation failed.
|
|
||||||
TLSAlertDecryptError = 51
|
|
||||||
|
|
||||||
// Sent by servers when no server exists identified by the name provided by the client via the "server_name" extension.
|
|
||||||
TLSUnrecognizedName = 112
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrDNSBogon indicates that we found a bogon address. This is the
|
// ErrDNSBogon indicates that we found a bogon address. This is the
|
||||||
// correct value with which to initialize MeasurementRoot.ErrDNSBogon
|
// correct value with which to initialize MeasurementRoot.ErrDNSBogon
|
||||||
// to tell this library to return an error when a bogon is found.
|
// to tell this library to return an error when a bogon is found.
|
||||||
|
@ -287,82 +161,6 @@ func toFailureString(err error) string {
|
||||||
return scrubber.Scrub(formatted) // scrub IP addresses in the error
|
return scrubber.Scrub(formatted) // scrub IP addresses in the error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClassifyQUICFailure is a classifier to translate QUIC errors to OONI error strings.
|
|
||||||
// TODO(kelmenhorst,bassosimone): Consider moving this into quicdialer.
|
|
||||||
func ClassifyQUICFailure(err error) string {
|
|
||||||
var versionNegotiation *quic.VersionNegotiationError
|
|
||||||
var statelessReset *quic.StatelessResetError
|
|
||||||
var handshakeTimeout *quic.HandshakeTimeoutError
|
|
||||||
var idleTimeout *quic.IdleTimeoutError
|
|
||||||
var transportError *quic.TransportError
|
|
||||||
|
|
||||||
if errors.As(err, &versionNegotiation) {
|
|
||||||
return FailureNoCompatibleQUICVersion
|
|
||||||
}
|
|
||||||
if errors.As(err, &statelessReset) {
|
|
||||||
return FailureConnectionReset
|
|
||||||
}
|
|
||||||
if errors.As(err, &handshakeTimeout) {
|
|
||||||
return FailureGenericTimeoutError
|
|
||||||
}
|
|
||||||
if errors.As(err, &idleTimeout) {
|
|
||||||
return FailureGenericTimeoutError
|
|
||||||
}
|
|
||||||
if errors.As(err, &transportError) {
|
|
||||||
if transportError.ErrorCode == quic.ConnectionRefused {
|
|
||||||
return FailureConnectionRefused
|
|
||||||
}
|
|
||||||
// the TLS Alert constants are taken from RFC8446
|
|
||||||
errCode := uint8(transportError.ErrorCode)
|
|
||||||
if isCertificateError(errCode) {
|
|
||||||
return FailureSSLInvalidCertificate
|
|
||||||
}
|
|
||||||
// TLSAlertDecryptError and TLSAlertHandshakeFailure are summarized to a FailureSSLHandshake error because both
|
|
||||||
// alerts are caused by a failed or corrupted parameter negotiation during the TLS handshake.
|
|
||||||
if errCode == TLSAlertDecryptError || errCode == TLSAlertHandshakeFailure {
|
|
||||||
return FailureSSLHandshake
|
|
||||||
}
|
|
||||||
if errCode == TLSAlertUnknownCA {
|
|
||||||
return FailureSSLUnknownAuthority
|
|
||||||
}
|
|
||||||
if errCode == TLSUnrecognizedName {
|
|
||||||
return FailureSSLInvalidHostname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return toFailureString(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClassifyResolveFailure is a classifier to translate DNS resolving errors to OONI error strings.
|
|
||||||
// TODO(kelmenhorst,bassosimone): Consider moving this into resolve.
|
|
||||||
func ClassifyResolveFailure(err error) string {
|
|
||||||
if errors.Is(err, ErrDNSBogon) {
|
|
||||||
return FailureDNSBogonError // not in MK
|
|
||||||
}
|
|
||||||
return toFailureString(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClassifyTLSFailure is a classifier to translate TLS errors to OONI error strings.
|
|
||||||
// TODO(kelmenhorst,bassosimone): Consider moving this into tlsdialer.
|
|
||||||
func ClassifyTLSFailure(err error) string {
|
|
||||||
var x509HostnameError x509.HostnameError
|
|
||||||
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 toFailureString(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toOperationString(err error, operation string) string {
|
func toOperationString(err error, operation string) string {
|
||||||
var errwrapper *ErrWrapper
|
var errwrapper *ErrWrapper
|
||||||
if errors.As(err, &errwrapper) {
|
if errors.As(err, &errwrapper) {
|
||||||
|
@ -393,11 +191,3 @@ func toOperationString(err error, operation string) string {
|
||||||
}
|
}
|
||||||
return operation
|
return operation
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCertificateError(alert uint8) bool {
|
|
||||||
return (alert == TLSAlertBadCertificate ||
|
|
||||||
alert == TLSAlertUnsupportedCertificate ||
|
|
||||||
alert == TLSAlertCertificateExpired ||
|
|
||||||
alert == TLSAlertCertificateRevoked ||
|
|
||||||
alert == TLSAlertCertificateUnknown)
|
|
||||||
}
|
|
||||||
|
|
|
@ -157,51 +157,51 @@ func TestToFailureString(t *testing.T) {
|
||||||
|
|
||||||
func TestClassifyQUICFailure(t *testing.T) {
|
func TestClassifyQUICFailure(t *testing.T) {
|
||||||
t.Run("for connection_reset", func(t *testing.T) {
|
t.Run("for connection_reset", func(t *testing.T) {
|
||||||
if ClassifyQUICFailure(&quic.StatelessResetError{}) != FailureConnectionReset {
|
if classifyQUICFailure(&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 ClassifyQUICFailure(&quic.VersionNegotiationError{}) != FailureNoCompatibleQUICVersion {
|
if classifyQUICFailure(&quic.VersionNegotiationError{}) != FailureNoCompatibleQUICVersion {
|
||||||
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 ClassifyQUICFailure(&quic.TransportError{ErrorCode: quic.ConnectionRefused}) != FailureConnectionRefused {
|
if classifyQUICFailure(&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 ClassifyQUICFailure(&quic.HandshakeTimeoutError{}) != FailureGenericTimeoutError {
|
if classifyQUICFailure(&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 ClassifyQUICFailure(&quic.IdleTimeoutError{}) != FailureGenericTimeoutError {
|
if classifyQUICFailure(&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 = TLSAlertHandshakeFailure
|
var err quic.TransportErrorCode = quicTLSAlertHandshakeFailure
|
||||||
if ClassifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLHandshake {
|
if classifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLHandshake {
|
||||||
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 = TLSAlertBadCertificate
|
var err quic.TransportErrorCode = quicTLSAlertBadCertificate
|
||||||
if ClassifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidCertificate {
|
if classifyQUICFailure(&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 = TLSAlertUnknownCA
|
var err quic.TransportErrorCode = quicTLSAlertUnknownCA
|
||||||
if ClassifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLUnknownAuthority {
|
if classifyQUICFailure(&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 = TLSUnrecognizedName
|
var err quic.TransportErrorCode = quicTLSUnrecognizedName
|
||||||
if ClassifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidHostname {
|
if classifyQUICFailure(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidHostname {
|
||||||
t.Fatal("unexpected results")
|
t.Fatal("unexpected results")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -210,7 +210,7 @@ func TestClassifyQUICFailure(t *testing.T) {
|
||||||
|
|
||||||
func TestClassifyResolveFailure(t *testing.T) {
|
func TestClassifyResolveFailure(t *testing.T) {
|
||||||
t.Run("for ErrDNSBogon", func(t *testing.T) {
|
t.Run("for ErrDNSBogon", func(t *testing.T) {
|
||||||
if ClassifyResolveFailure(ErrDNSBogon) != FailureDNSBogonError {
|
if classifyResolveFailure(ErrDNSBogon) != FailureDNSBogonError {
|
||||||
t.Fatal("unexpected result")
|
t.Fatal("unexpected result")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -219,19 +219,19 @@ func TestClassifyResolveFailure(t *testing.T) {
|
||||||
func TestClassifyTLSFailure(t *testing.T) {
|
func TestClassifyTLSFailure(t *testing.T) {
|
||||||
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 ClassifyTLSFailure(err) != FailureSSLInvalidHostname {
|
if classifyTLSFailure(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 ClassifyTLSFailure(err) != FailureSSLUnknownAuthority {
|
if classifyTLSFailure(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 ClassifyTLSFailure(err) != FailureSSLInvalidCertificate {
|
if classifyTLSFailure(err) != FailureSSLInvalidCertificate {
|
||||||
t.Fatal("unexpected result")
|
t.Fatal("unexpected result")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
48
internal/errorsx/failures.go
Normal file
48
internal/errorsx/failures.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package errorsx
|
||||||
|
|
||||||
|
// This enumeration lists all the failures defined at
|
||||||
|
// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md
|
||||||
|
const (
|
||||||
|
// FailureConnectionRefused means ECONNREFUSED.
|
||||||
|
FailureConnectionRefused = "connection_refused"
|
||||||
|
|
||||||
|
// FailureConnectionReset means ECONNRESET.
|
||||||
|
FailureConnectionReset = "connection_reset"
|
||||||
|
|
||||||
|
// FailureDNSBogonError means we detected bogon in DNS reply.
|
||||||
|
FailureDNSBogonError = "dns_bogon_error"
|
||||||
|
|
||||||
|
// FailureDNSNXDOMAINError means we got NXDOMAIN in DNS reply.
|
||||||
|
FailureDNSNXDOMAINError = "dns_nxdomain_error"
|
||||||
|
|
||||||
|
// FailureEOFError means we got unexpected EOF on connection.
|
||||||
|
FailureEOFError = "eof_error"
|
||||||
|
|
||||||
|
// FailureGenericTimeoutError means we got some timer has expired.
|
||||||
|
FailureGenericTimeoutError = "generic_timeout_error"
|
||||||
|
|
||||||
|
// FailureHostUnreachable means that there is "no route to host".
|
||||||
|
FailureHostUnreachable = "host_unreachable"
|
||||||
|
|
||||||
|
// FailureInterrupted means that the user interrupted us.
|
||||||
|
FailureInterrupted = "interrupted"
|
||||||
|
|
||||||
|
// FailureNoCompatibleQUICVersion means that the server does not support the proposed QUIC version
|
||||||
|
FailureNoCompatibleQUICVersion = "quic_incompatible_version"
|
||||||
|
|
||||||
|
// FailureSSLHandshake means that the negotiation of cryptographic parameters failed
|
||||||
|
FailureSSLHandshake = "ssl_failed_handshake"
|
||||||
|
|
||||||
|
// FailureSSLInvalidHostname means we got certificate is not valid for SNI.
|
||||||
|
FailureSSLInvalidHostname = "ssl_invalid_hostname"
|
||||||
|
|
||||||
|
// FailureSSLUnknownAuthority means we cannot find CA validating certificate.
|
||||||
|
FailureSSLUnknownAuthority = "ssl_unknown_authority"
|
||||||
|
|
||||||
|
// FailureSSLInvalidCertificate means certificate experired or other
|
||||||
|
// sort of errors causing it to be invalid.
|
||||||
|
FailureSSLInvalidCertificate = "ssl_invalid_certificate"
|
||||||
|
|
||||||
|
// FailureJSONParseError indicates that we couldn't parse a JSON
|
||||||
|
FailureJSONParseError = "json_parse_error"
|
||||||
|
)
|
44
internal/errorsx/operations.go
Normal file
44
internal/errorsx/operations.go
Normal 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"
|
||||||
|
)
|
|
@ -3,6 +3,7 @@ package errorsx
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
"github.com/lucas-clemente/quic-go"
|
||||||
|
@ -86,7 +87,7 @@ func (d *ErrorWrapperQUICDialer) DialContext(
|
||||||
tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) {
|
tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) {
|
||||||
sess, err := d.Dialer.DialContext(ctx, network, host, tlsCfg, cfg)
|
sess, err := d.Dialer.DialContext(ctx, network, host, tlsCfg, cfg)
|
||||||
err = SafeErrWrapperBuilder{
|
err = SafeErrWrapperBuilder{
|
||||||
Classifier: ClassifyQUICFailure,
|
Classifier: classifyQUICFailure,
|
||||||
Error: err,
|
Error: err,
|
||||||
Operation: QUICHandshakeOperation,
|
Operation: QUICHandshakeOperation,
|
||||||
}.MaybeBuild()
|
}.MaybeBuild()
|
||||||
|
@ -95,3 +96,85 @@ func (d *ErrorWrapperQUICDialer) DialContext(
|
||||||
}
|
}
|
||||||
return sess, nil
|
return sess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// classifyQUICFailure is a classifier to translate QUIC errors to OONI error strings.
|
||||||
|
func classifyQUICFailure(err error) string {
|
||||||
|
var versionNegotiation *quic.VersionNegotiationError
|
||||||
|
var statelessReset *quic.StatelessResetError
|
||||||
|
var handshakeTimeout *quic.HandshakeTimeoutError
|
||||||
|
var idleTimeout *quic.IdleTimeoutError
|
||||||
|
var transportError *quic.TransportError
|
||||||
|
|
||||||
|
if errors.As(err, &versionNegotiation) {
|
||||||
|
return FailureNoCompatibleQUICVersion
|
||||||
|
}
|
||||||
|
if errors.As(err, &statelessReset) {
|
||||||
|
return FailureConnectionReset
|
||||||
|
}
|
||||||
|
if errors.As(err, &handshakeTimeout) {
|
||||||
|
return FailureGenericTimeoutError
|
||||||
|
}
|
||||||
|
if errors.As(err, &idleTimeout) {
|
||||||
|
return FailureGenericTimeoutError
|
||||||
|
}
|
||||||
|
if errors.As(err, &transportError) {
|
||||||
|
if transportError.ErrorCode == quic.ConnectionRefused {
|
||||||
|
return FailureConnectionRefused
|
||||||
|
}
|
||||||
|
// the TLS Alert constants are taken from RFC8446
|
||||||
|
errCode := uint8(transportError.ErrorCode)
|
||||||
|
if 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 FailureSSLHandshake
|
||||||
|
}
|
||||||
|
if errCode == quicTLSAlertUnknownCA {
|
||||||
|
return FailureSSLUnknownAuthority
|
||||||
|
}
|
||||||
|
if errCode == quicTLSUnrecognizedName {
|
||||||
|
return FailureSSLInvalidHostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toFailureString(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package errorsx
|
package errorsx
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
// Resolver is a DNS resolver. The *net.Resolver used by Go implements
|
// Resolver is a DNS resolver. The *net.Resolver used by Go implements
|
||||||
// this interface, but other implementations are possible.
|
// this interface, but other implementations are possible.
|
||||||
|
@ -20,13 +23,21 @@ var _ Resolver = &ErrorWrapperResolver{}
|
||||||
func (r *ErrorWrapperResolver) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
func (r *ErrorWrapperResolver) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
||||||
addrs, err := r.Resolver.LookupHost(ctx, hostname)
|
addrs, err := r.Resolver.LookupHost(ctx, hostname)
|
||||||
err = SafeErrWrapperBuilder{
|
err = SafeErrWrapperBuilder{
|
||||||
Classifier: ClassifyResolveFailure,
|
Classifier: classifyResolveFailure,
|
||||||
Error: err,
|
Error: err,
|
||||||
Operation: ResolveOperation,
|
Operation: ResolveOperation,
|
||||||
}.MaybeBuild()
|
}.MaybeBuild()
|
||||||
return addrs, err
|
return addrs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// classifyResolveFailure is a classifier to translate DNS resolving errors to OONI error strings.
|
||||||
|
func classifyResolveFailure(err error) string {
|
||||||
|
if errors.Is(err, ErrDNSBogon) {
|
||||||
|
return FailureDNSBogonError // not in MK
|
||||||
|
}
|
||||||
|
return toFailureString(err)
|
||||||
|
}
|
||||||
|
|
||||||
type resolverNetworker interface {
|
type resolverNetworker interface {
|
||||||
Network() string
|
Network() string
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package errorsx
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,9 +25,30 @@ func (h *ErrorWrapperTLSHandshaker) Handshake(
|
||||||
) (net.Conn, tls.ConnectionState, error) {
|
) (net.Conn, tls.ConnectionState, error) {
|
||||||
tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config)
|
tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config)
|
||||||
err = SafeErrWrapperBuilder{
|
err = SafeErrWrapperBuilder{
|
||||||
Classifier: ClassifyTLSFailure,
|
Classifier: classifyTLSFailure,
|
||||||
Error: err,
|
Error: err,
|
||||||
Operation: TLSHandshakeOperation,
|
Operation: TLSHandshakeOperation,
|
||||||
}.MaybeBuild()
|
}.MaybeBuild()
|
||||||
return tlsconn, state, err
|
return tlsconn, state, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// classifyTLSFailure is a classifier to translate TLS errors to OONI error strings.
|
||||||
|
func classifyTLSFailure(err error) string {
|
||||||
|
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 toFailureString(err)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user