forked from ooni/probe-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclassify.go
349 lines (310 loc) · 11.6 KB
/
classify.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package netxlite
//
// Mapping Go errors to OONI errors
//
import (
"context"
"crypto/x509"
"errors"
"fmt"
"strings"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/scrubber"
)
// ClassifyGenericError maps an error occurred during an operation to
// an OONI failure string. This specific classifier is the most
// generic one. You usually use it when mapping I/O errors. You should
// check whether there is a specific classifier for more specific
// operations (e.g., DNS resolution, TLS handshake).
//
// If the input error is an *ErrWrapper we don't perform
// the classification again and we return its Failure.
//
// 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 the original error string.
func ClassifyGenericError(err error) string {
// The list returned here matches the values used by MK unless
// explicitly noted otherwise with a comment.
// Robustness: handle the case where we're passed a wrapped error.
var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) {
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 != "" {
return failure
}
if errors.Is(err, context.Canceled) {
return FailureInterrupted
}
if failure := classifyWithStringSuffix(err); failure != "" {
return failure
}
formatted := fmt.Sprintf("unknown_failure: %s", err.Error())
return scrubber.Scrub(formatted) // scrub IP addresses in the error
}
// classifyWithStringSuffix is a subset of ClassifyGenericError that
// performs classification by looking at error suffixes. This function
// will return an empty string if it cannot classify the error.
func classifyWithStringSuffix(err error) string {
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
}
if strings.HasSuffix(s, "TLS handshake timeout") {
return FailureGenericTimeoutError
}
if strings.HasSuffix(s, DNSNoSuchHostSuffix) {
// 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
}
if strings.HasSuffix(s, DNSServerMisbehavingSuffix) {
return FailureDNSServerMisbehaving
}
if strings.HasSuffix(s, DNSNoAnswerSuffix) {
return FailureDNSNoAnswer
}
if strings.HasSuffix(s, "use of closed network connection") {
return FailureConnectionAlreadyClosed
}
return "" // not found
}
// TLS alert protocol as defined in RFC8446. We need these definitions
// to figure out which error occurred during a QUIC handshake.
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
)
// ClassifyQUICHandshakeError maps errors during a QUIC
// handshake to OONI failure strings.
//
// If the input error is an *ErrWrapper we don't perform
// the classification again and we return its Failure.
//
// If this classifier fails, it calls ClassifyGenericError
// and returns to the caller its return value.
func ClassifyQUICHandshakeError(err error) string {
// Robustness: handle the case where we're passed a wrapped error.
var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) {
return errwrapper.Error() // we've already wrapped it
}
var (
versionNegotiation *quic.VersionNegotiationError
statelessReset *quic.StatelessResetError
handshakeTimeout *quic.HandshakeTimeoutError
idleTimeout *quic.IdleTimeoutError
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
}
// quic.TransportError wraps OONI errors using the error
// code quic.InternalError. So, if the error code is
// an internal error, search for a OONI error and, if
// found, just return such an error.
if transportError.ErrorCode == quic.InternalError {
if s := failuresMap[transportError.ErrorMessage]; s != "" {
return s
}
}
}
return ClassifyGenericError(err)
}
// 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 {
// List out each case separately so we know we test them
switch alert {
case quicTLSAlertBadCertificate:
return true
case quicTLSAlertUnsupportedCertificate:
return true
case quicTLSAlertCertificateExpired:
return true
case quicTLSAlertCertificateRevoked:
return true
case quicTLSAlertCertificateUnknown:
return true
default:
return false
}
}
// ErrDNSBogon indicates that we found a bogon address. Code that
// filters for DNS bogons MUST use this error.
var ErrDNSBogon = errors.New("dns: detected bogon address")
// We use these strings to string-match errors in the standard library
// and map such errors to OONI failures.
const (
DNSNoSuchHostSuffix = "no such host"
DNSServerMisbehavingSuffix = "server misbehaving"
DNSNoAnswerSuffix = "no answer from DNS server"
)
// These errors are returned by custom DNSTransport instances (e.g.,
// DNSOverHTTPSTransport and DNSOverUDPTransport). Their suffix matches the equivalent
// unexported errors used by the Go standard library.
var (
// ErrOODNSNoSuchHost means NXDOMAIN.
ErrOODNSNoSuchHost = fmt.Errorf("ooniresolver: %s", DNSNoSuchHostSuffix)
// ErrOODNSMisbehaving is the error typically returned by the `netgo`resolver
// when it cannot really make sense of the error.
ErrOODNSMisbehaving = fmt.Errorf("ooniresolver: %s", DNSServerMisbehavingSuffix)
// ErrOODNSNoAnswer means that we've got a valid DNS response that
// did not contain any answer for the original query. This could happen
// when we query for AAAA and the domain only has A records.
ErrOODNSNoAnswer = fmt.Errorf("ooniresolver: %s", DNSNoAnswerSuffix)
)
// These errors are not part of the Go standard library but we can
// return them in our custom resolvers.
var (
// ErrOODNSRefused indicates that the response's Rcode was "refused"
ErrOODNSRefused = errors.New("ooniresolver: refused")
// ErrOODNSServfail indicates that the response's Rcode was "servfail"
ErrOODNSServfail = errors.New("ooniresolver: servfail")
)
// ErrAndroidDNSCacheNoData is the kind of error returned by our getaddrinfo
// code on Android when we see EAI_NODATA, an error condition that could mean
// anything as explained in getaddrinfo_linux.go.
var ErrAndroidDNSCacheNoData = errors.New(FailureAndroidDNSCacheNoData)
// ClassifyResolverError maps DNS resolution errors to
// OONI failure strings.
//
// If the input error is an *ErrWrapper we don't perform
// the classification again and we return its Failure.
//
// If this classifier fails, it calls ClassifyGenericError and
// returns to the caller its return value.
func ClassifyResolverError(err error) string {
// Robustness: handle the case where we're passed a wrapped error.
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
}
// Implementation note: we match errors that share the same
// string of the stdlib in the generic classifier.
if errors.Is(err, ErrOODNSRefused) {
return FailureDNSRefusedError // not in MK
}
if errors.Is(err, ErrOODNSServfail) {
return FailureDNSServfailError
}
if errors.Is(err, ErrDNSReplyWithWrongQueryID) {
return FailureDNSReplyWithWrongQueryID
}
if errors.Is(err, ErrAndroidDNSCacheNoData) {
return FailureAndroidDNSCacheNoData
}
return ClassifyGenericError(err)
}
// ClassifyTLSHandshakeError maps an error occurred during the TLS
// handshake to an OONI failure string.
//
// If the input error is an *ErrWrapper we don't perform
// the classification again and we return its Failure.
//
// If this classifier fails, it calls ClassifyGenericError and
// returns to the caller its return value.
func ClassifyTLSHandshakeError(err error) string {
// Robustness: handle the case where we're passed a wrapped error.
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)
}