1// Package errorx contains error extensions
2package errorx
3
4import (
5	"context"
6	"crypto/x509"
7	"errors"
8	"fmt"
9	"regexp"
10	"strings"
11)
12
13const (
14	// FailureConnectionRefused means ECONNREFUSED.
15	FailureConnectionRefused = "connection_refused"
16
17	// FailureConnectionReset means ECONNRESET.
18	FailureConnectionReset = "connection_reset"
19
20	// FailureDNSBogonError means we detected bogon in DNS reply.
21	FailureDNSBogonError = "dns_bogon_error"
22
23	// FailureDNSNXDOMAINError means we got NXDOMAIN in DNS reply.
24	FailureDNSNXDOMAINError = "dns_nxdomain_error"
25
26	// FailureEOFError means we got unexpected EOF on connection.
27	FailureEOFError = "eof_error"
28
29	// FailureGenericTimeoutError means we got some timer has expired.
30	FailureGenericTimeoutError = "generic_timeout_error"
31
32	// FailureInterrupted means that the user interrupted us.
33	FailureInterrupted = "interrupted"
34
35	// FailureNoCompatibleQUICVersion means that the server does not support the proposed QUIC version
36	FailureNoCompatibleQUICVersion = "quic_incompatible_version"
37
38	// FailureSSLInvalidHostname means we got certificate is not valid for SNI.
39	FailureSSLInvalidHostname = "ssl_invalid_hostname"
40
41	// FailureSSLUnknownAuthority means we cannot find CA validating certificate.
42	FailureSSLUnknownAuthority = "ssl_unknown_authority"
43
44	// FailureSSLInvalidCertificate means certificate experired or other
45	// sort of errors causing it to be invalid.
46	FailureSSLInvalidCertificate = "ssl_invalid_certificate"
47
48	// FailureJSONParseError indicates that we couldn't parse a JSON
49	FailureJSONParseError = "json_parse_error"
50)
51
52const (
53	// ResolveOperation is the operation where we resolve a domain name
54	ResolveOperation = "resolve"
55
56	// ConnectOperation is the operation where we do a TCP connect
57	ConnectOperation = "connect"
58
59	// TLSHandshakeOperation is the TLS handshake
60	TLSHandshakeOperation = "tls_handshake"
61
62	// QUICHandshakeOperation is the handshake to setup a QUIC connection
63	QUICHandshakeOperation = "quic_handshake"
64
65	// HTTPRoundTripOperation is the HTTP round trip
66	HTTPRoundTripOperation = "http_round_trip"
67
68	// CloseOperation is when we close a socket
69	CloseOperation = "close"
70
71	// ReadOperation is when we read from a socket
72	ReadOperation = "read"
73
74	// WriteOperation is when we write to a socket
75	WriteOperation = "write"
76
77	// ReadFromOperation is when we read from an UDP socket
78	ReadFromOperation = "read_from"
79
80	// WriteToOperation is when we write to an UDP socket
81	WriteToOperation = "write_to"
82
83	// UnknownOperation is when we cannot determine the operation
84	UnknownOperation = "unknown"
85
86	// TopLevelOperation is used when the failure happens at top level. This
87	// happens for example with urlgetter with a cancelled context.
88	TopLevelOperation = "top_level"
89)
90
91// ErrDNSBogon indicates that we found a bogon address. This is the
92// correct value with which to initialize MeasurementRoot.ErrDNSBogon
93// to tell this library to return an error when a bogon is found.
94var ErrDNSBogon = errors.New("dns: detected bogon address")
95
96// ErrWrapper is our error wrapper for Go errors. The key objective of
97// this structure is to properly set Failure, which is also returned by
98// the Error() method, so be one of the OONI defined strings.
99type ErrWrapper struct {
100	// ConnID is the connection ID, or zero if not known.
101	ConnID int64
102
103	// DialID is the dial ID, or zero if not known.
104	DialID int64
105
106	// Failure is the OONI failure string. The failure strings are
107	// loosely backward compatible with Measurement Kit.
108	//
109	// This is either one of the FailureXXX strings or any other
110	// string like `unknown_failure ...`. The latter represents an
111	// error that we have not yet mapped to a failure.
112	Failure string
113
114	// Operation is the operation that failed. If possible, it
115	// SHOULD be a _major_ operation. Major operations are:
116	//
117	// - ResolveOperation: resolving a domain name failed
118	// - ConnectOperation: connecting to an IP failed
119	// - TLSHandshakeOperation: TLS handshaking failed
120	// - HTTPRoundTripOperation: other errors during round trip
121	//
122	// Because a network connection doesn't necessarily know
123	// what is the current major operation we also have the
124	// following _minor_ operations:
125	//
126	// - CloseOperation: CLOSE failed
127	// - ReadOperation: READ failed
128	// - WriteOperation: WRITE failed
129	//
130	// If an ErrWrapper referring to a major operation is wrapping
131	// another ErrWrapper and such ErrWrapper already refers to
132	// a major operation, then the new ErrWrapper should use the
133	// child ErrWrapper major operation. Otherwise, it should use
134	// its own major operation. This way, the topmost wrapper is
135	// supposed to refer to the major operation that failed.
136	Operation string
137
138	// TransactionID is the transaction ID, or zero if not known.
139	TransactionID int64
140
141	// WrappedErr is the error that we're wrapping.
142	WrappedErr error
143}
144
145// Error returns a description of the error that occurred.
146func (e *ErrWrapper) Error() string {
147	return e.Failure
148}
149
150// Unwrap allows to access the underlying error
151func (e *ErrWrapper) Unwrap() error {
152	return e.WrappedErr
153}
154
155// SafeErrWrapperBuilder contains a builder for ErrWrapper that
156// is safe, i.e., behaves correctly when the error is nil.
157type SafeErrWrapperBuilder struct {
158	// ConnID is the connection ID, if any
159	ConnID int64
160
161	// DialID is the dial ID, if any
162	DialID int64
163
164	// Error is the error, if any
165	Error error
166
167	// Operation is the operation that failed
168	Operation string
169
170	// TransactionID is the transaction ID, if any
171	TransactionID int64
172}
173
174// MaybeBuild builds a new ErrWrapper, if b.Error is not nil, and returns
175// a nil error value, instead, if b.Error is nil.
176func (b SafeErrWrapperBuilder) MaybeBuild() (err error) {
177	if b.Error != nil {
178		err = &ErrWrapper{
179			ConnID:        b.ConnID,
180			DialID:        b.DialID,
181			Failure:       toFailureString(b.Error),
182			Operation:     toOperationString(b.Error, b.Operation),
183			TransactionID: b.TransactionID,
184			WrappedErr:    b.Error,
185		}
186	}
187	return
188}
189
190func toFailureString(err error) string {
191	// The list returned here matches the values used by MK unless
192	// explicitly noted otherwise with a comment.
193
194	var errwrapper *ErrWrapper
195	if errors.As(err, &errwrapper) {
196		return errwrapper.Error() // we've already wrapped it
197	}
198
199	if errors.Is(err, ErrDNSBogon) {
200		return FailureDNSBogonError // not in MK
201	}
202	if errors.Is(err, context.Canceled) {
203		return FailureInterrupted
204	}
205	var x509HostnameError x509.HostnameError
206	if errors.As(err, &x509HostnameError) {
207		// Test case: https://wrong.host.badssl.com/
208		return FailureSSLInvalidHostname
209	}
210	var x509UnknownAuthorityError x509.UnknownAuthorityError
211	if errors.As(err, &x509UnknownAuthorityError) {
212		// Test case: https://self-signed.badssl.com/. This error has
213		// never been among the ones returned by MK.
214		return FailureSSLUnknownAuthority
215	}
216	var x509CertificateInvalidError x509.CertificateInvalidError
217	if errors.As(err, &x509CertificateInvalidError) {
218		// Test case: https://expired.badssl.com/
219		return FailureSSLInvalidCertificate
220	}
221
222	s := err.Error()
223	if strings.HasSuffix(s, "operation was canceled") {
224		return FailureInterrupted
225	}
226	if strings.HasSuffix(s, "EOF") {
227		return FailureEOFError
228	}
229	if strings.HasSuffix(s, "connection refused") {
230		return FailureConnectionRefused
231	}
232	if strings.HasSuffix(s, "connection reset by peer") {
233		return FailureConnectionReset
234	}
235	if strings.HasSuffix(s, "context deadline exceeded") {
236		return FailureGenericTimeoutError
237	}
238	if strings.HasSuffix(s, "transaction is timed out") {
239		return FailureGenericTimeoutError
240	}
241	if strings.HasSuffix(s, "i/o timeout") {
242		return FailureGenericTimeoutError
243	}
244	if strings.HasSuffix(s, "TLS handshake timeout") {
245		return FailureGenericTimeoutError
246	}
247	if strings.HasSuffix(s, "no such host") {
248		// This is dns_lookup_error in MK but such error is used as a
249		// generic "hey, the lookup failed" error. Instead, this error
250		// that we return here is significantly more specific.
251		return FailureDNSNXDOMAINError
252	}
253
254	// TODO(kelmenhorst): see whether it is possible to match errors
255	// from qtls rather than strings for TLS errors below.
256	//
257	// TODO(kelmenhorst): make sure we have tests for all errors. Also,
258	// how to ensure we are robust to changes in other libs?
259	//
260	// special QUIC errors
261	matched, err := regexp.MatchString(`.*x509: certificate is valid for.*not.*`, s)
262	if matched {
263		return FailureSSLInvalidHostname
264	}
265	if strings.HasSuffix(s, "x509: certificate signed by unknown authority") {
266		return FailureSSLUnknownAuthority
267	}
268	certInvalidErrors := []string{"x509: certificate is not authorized to sign other certificates", "x509: certificate has expired or is not yet valid:", "x509: a root or intermediate certificate is not authorized to sign for this name:", "x509: a root or intermediate certificate is not authorized for an extended key usage:", "x509: too many intermediates for path length constraint", "x509: certificate specifies an incompatible key usage", "x509: issuer name does not match subject from issuing certificate", "x509: issuer has name constraints but leaf doesn't have a SAN extension", "x509: issuer has name constraints but leaf contains unknown or unconstrained name:"}
269	for _, errstr := range certInvalidErrors {
270		if strings.Contains(s, errstr) {
271			return FailureSSLInvalidCertificate
272		}
273	}
274	if strings.HasPrefix(s, "No compatible QUIC version found") {
275		return FailureNoCompatibleQUICVersion
276	}
277	if strings.HasSuffix(s, "Handshake did not complete in time") {
278		return FailureGenericTimeoutError
279	}
280	if strings.HasSuffix(s, "connection_refused") {
281		return FailureConnectionRefused
282	}
283	if strings.Contains(s, "stateless_reset") {
284		return FailureConnectionReset
285	}
286	if strings.Contains(s, "deadline exceeded") {
287		return FailureGenericTimeoutError
288	}
289	formatted := fmt.Sprintf("unknown_failure: %s", s)
290	return Scrub(formatted) // scrub IP addresses in the error
291}
292
293func toOperationString(err error, operation string) string {
294	var errwrapper *ErrWrapper
295	if errors.As(err, &errwrapper) {
296		// Basically, as explained in ErrWrapper docs, let's
297		// keep the child major operation, if any.
298		if errwrapper.Operation == ConnectOperation {
299			return errwrapper.Operation
300		}
301		if errwrapper.Operation == HTTPRoundTripOperation {
302			return errwrapper.Operation
303		}
304		if errwrapper.Operation == ResolveOperation {
305			return errwrapper.Operation
306		}
307		if errwrapper.Operation == TLSHandshakeOperation {
308			return errwrapper.Operation
309		}
310		if errwrapper.Operation == QUICHandshakeOperation {
311			return errwrapper.Operation
312		}
313		if errwrapper.Operation == "quic_handshake_start" {
314			return QUICHandshakeOperation
315		}
316		if errwrapper.Operation == "quic_handshake_done" {
317			return QUICHandshakeOperation
318		}
319		// FALLTHROUGH
320	}
321	return operation
322}
323