5ebdeb56ca
## Checklist - [x] I have read the [contribution guidelines](https://github.com/ooni/probe-cli/blob/master/CONTRIBUTING.md) - [x] reference issue for this pull request: https://github.com/ooni/probe/issues/2158 - [x] if you changed anything related how experiments work and you need to reflect these changes in the ooni/spec repository, please link to the related ooni/spec pull request: https://github.com/ooni/spec/pull/250 ## Description This diff refactors the codebase to reimplement tlsping and tcpping to use the step-by-step measurements style. See docs/design/dd-003-step-by-step.md for more information on the step-by-step measurement style.
165 lines
5.2 KiB
Go
165 lines
5.2 KiB
Go
package netxlite
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
)
|
|
|
|
// ErrWrapper is our error wrapper for Go errors. The key objective of
|
|
// this structure is to properly set Failure, which is also returned by
|
|
// 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 {
|
|
// Failure is the OONI failure string. The failure strings are
|
|
// loosely backward compatible with Measurement Kit.
|
|
//
|
|
// This is either one of the FailureXXX strings or any other
|
|
// string like `unknown_failure: ...`. The latter represents an
|
|
// error that we have not yet mapped to a failure.
|
|
Failure string
|
|
|
|
// Operation is the operation that failed.
|
|
//
|
|
// If possible, the Operation string SHOULD be a _major_
|
|
// operation. Major operations are:
|
|
//
|
|
// - ResolveOperation: resolving a domain name failed
|
|
// - ConnectOperation: connecting to an IP failed
|
|
// - TLSHandshakeOperation: TLS handshaking failed
|
|
// - QUICHandshakeOperation: QUIC handshaking failed
|
|
// - HTTPRoundTripOperation: other errors during round trip
|
|
//
|
|
// Because a network connection doesn't necessarily know
|
|
// what is the current major operation we also have the
|
|
// following _minor_ operations:
|
|
//
|
|
// - CloseOperation: CLOSE failed
|
|
// - ReadOperation: READ failed
|
|
// - WriteOperation: WRITE failed
|
|
//
|
|
// If an ErrWrapper referring to a major operation is wrapping
|
|
// another ErrWrapper and such ErrWrapper already refers to
|
|
// a major operation, then the new ErrWrapper should use the
|
|
// child ErrWrapper major operation. Otherwise, it should use
|
|
// its own major operation. This way, the topmost wrapper is
|
|
// supposed to refer to the major operation that failed.
|
|
Operation string
|
|
|
|
// WrappedErr is the error that we're wrapping.
|
|
WrappedErr error
|
|
}
|
|
|
|
// Error returns the OONI failure string for this error.
|
|
func (e *ErrWrapper) Error() string {
|
|
return e.Failure
|
|
}
|
|
|
|
// Unwrap allows to access the underlying error.
|
|
func (e *ErrWrapper) Unwrap() error {
|
|
return e.WrappedErr
|
|
}
|
|
|
|
// MarshalJSON converts an ErrWrapper to a JSON value.
|
|
func (e *ErrWrapper) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(e.Failure)
|
|
}
|
|
|
|
// classifier is the type of the function that maps a Go error
|
|
// to a OONI failure string defined at
|
|
// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md.
|
|
type classifier func(err error) string
|
|
|
|
// NewErrWrapper creates a new ErrWrapper using the given
|
|
// classifier, operation name, and underlying error.
|
|
//
|
|
// This function panics if classifier is nil, or operation
|
|
// is the empty string or error is nil.
|
|
//
|
|
// If the err argument has already been classified, the returned
|
|
// error wrapper will use the same classification string and
|
|
// will determine whether to keep the major operation as documented
|
|
// in the ErrWrapper.Operation documentation.
|
|
func NewErrWrapper(c classifier, op string, err error) *ErrWrapper {
|
|
var wrapper *ErrWrapper
|
|
if errors.As(err, &wrapper) {
|
|
return &ErrWrapper{
|
|
Failure: wrapper.Failure,
|
|
Operation: classifyOperation(wrapper, op),
|
|
WrappedErr: err,
|
|
}
|
|
}
|
|
if c == nil {
|
|
panic("nil classifier")
|
|
}
|
|
if op == "" {
|
|
panic("empty op")
|
|
}
|
|
if err == nil {
|
|
panic("nil err")
|
|
}
|
|
return &ErrWrapper{
|
|
Failure: c(err),
|
|
Operation: op,
|
|
WrappedErr: err,
|
|
}
|
|
}
|
|
|
|
// TODO(https://github.com/ooni/probe/issues/2163): we can really
|
|
// simplify the error wrapping situation here by just dropping
|
|
// NewErrWrapper and always using MaybeNewErrWrapper.
|
|
|
|
// MaybeNewErrWrapper is like NewErrWrapper except that this
|
|
// function won't panic if passed a nil error.
|
|
func MaybeNewErrWrapper(c classifier, op string, err error) error {
|
|
if err != nil {
|
|
return NewErrWrapper(c, op, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewTopLevelGenericErrWrapper wraps an error occurring at top
|
|
// level using a generic classifier as classifier. This is the
|
|
// function you should call when you suspect a given error hasn't
|
|
// already been wrapped. This function panics if err is nil.
|
|
//
|
|
// If the err argument has already been classified, the returned
|
|
// error wrapper will use the same classification string and
|
|
// failed operation of the original error.
|
|
func NewTopLevelGenericErrWrapper(err error) *ErrWrapper {
|
|
return NewErrWrapper(ClassifyGenericError, TopLevelOperation, err)
|
|
}
|
|
|
|
func classifyOperation(ew *ErrWrapper, operation string) string {
|
|
// Basically, as explained in ErrWrapper docs, let's
|
|
// keep the child major operation, if any.
|
|
//
|
|
// QUIRK: this code is legacy code and we should not change
|
|
// it unless we also change the experiments that depend on it
|
|
// for determining the blocking reason based on the failed
|
|
// operation value (e.g., telegram, web connectivity).
|
|
if ew.Operation == ConnectOperation {
|
|
return ew.Operation
|
|
}
|
|
if ew.Operation == HTTPRoundTripOperation {
|
|
return ew.Operation
|
|
}
|
|
if ew.Operation == ResolveOperation {
|
|
return ew.Operation
|
|
}
|
|
if ew.Operation == TLSHandshakeOperation {
|
|
return ew.Operation
|
|
}
|
|
if ew.Operation == QUICHandshakeOperation {
|
|
return ew.Operation
|
|
}
|
|
if ew.Operation == "quic_handshake_start" {
|
|
return QUICHandshakeOperation
|
|
}
|
|
if ew.Operation == "quic_handshake_done" {
|
|
return QUICHandshakeOperation
|
|
}
|
|
return operation
|
|
}
|