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