410 lines
13 KiB
Markdown
410 lines
13 KiB
Markdown
# OONI Network Extensions
|
|
|
|
| | |
|
|
|:-------------|:-------------|
|
|
| Author | [@bassosimone](https://github.com/bassosimone) |
|
|
| Last-Updated | 2020-04-02 |
|
|
| Status | historic |
|
|
| Obsoleted-by | [ooni/probe-engine#359](https://github.com/ooni/probe-engine/issues/359) |
|
|
| Obsoleted-by | [ooni/probe-engine#522](https://github.com/ooni/probe-engine/pull/522) |
|
|
| Obsoleted-by | [dd-003-step-by-step.md](dd-003-step-by-step.md) |
|
|
|
|
*Abstract.* Rationale and design of [ooni/netx](https://github.com/ooni/netx),
|
|
which was later merged into [ooni/probe-engine](
|
|
https://github.com/ooni/probe-engine) and
|
|
[ooni/probe-cli](https://github.com/ooni/probe-cli).
|
|
|
|
## Introduction
|
|
|
|
OONI experiments send and/or receive network traffic to
|
|
determine if there is blocking. We want the implementation
|
|
of OONI experiments to be as simple as possible. We also
|
|
_want to attribute errors to the major network or protocol
|
|
operation that caused them_.
|
|
|
|
At the same time, _we want an experiment to collect as much
|
|
low-level data as possible_. For example, we want to know
|
|
whether and when the TLS handshake completed; what certificates
|
|
were provided by the server; what TLS version was selected;
|
|
and so forth. These bits of information are very useful
|
|
to analyze a measurement and better classify it.
|
|
|
|
We also want to _automatically or manually run follow-up
|
|
measurements where we change some configuration properties
|
|
and repeat the measurement_. For example, we may want to
|
|
configure DNS over HTTPS (DoH) and then attempt to
|
|
fetch again an URL. Or we may want to detect whether
|
|
there is SNI bases blocking. This package allows us to
|
|
do that in other parts of probe-engine.
|
|
|
|
## Rationale
|
|
|
|
As we observed [ooni/probe-engine#13](
|
|
https://github.com/ooni/probe-engine/issues/13), every
|
|
experiment consists of two separate phases:
|
|
|
|
1. measurement gathering
|
|
|
|
2. measurement analysis
|
|
|
|
During measurement gathering, we perform specific actions
|
|
that cause network data to be sent and/or received. During
|
|
measurement analysis, we process the measurement on the
|
|
device. For some experiments (e.g., Web Connectivity), this
|
|
second phase also entails contacting OONI backend services
|
|
that provide data useful to complete the analysis.
|
|
|
|
This package implements measurement gathering. The analysis
|
|
is performed by other packages in probe-engine. The core
|
|
design idea is to provide OONI-measurements-aware replacements
|
|
for Go standard library interfaces, e.g., the
|
|
`http.RoundTripper`. On top of that, we'll create all the
|
|
required interfaces to achive the measurement goals mentioned above.
|
|
|
|
We are of course writing test templates in `probe-engine`
|
|
anyway, because we need additional abstraction, but we can
|
|
take advantage of the fact that the API exposed by this package
|
|
is stable by definition, because it mimics the stdlib. Also,
|
|
for many experiments we can collect information pertaining
|
|
to TCP, DNS, TLS, and HTTP with a single call to `netx`.
|
|
|
|
This code used to live at `github.com/ooni/netx`. On 2020-03-02
|
|
we merged github.com/ooni/netx@4f8d645bce6466bb into `probe-engine`
|
|
because it was more practical and enabled easier refactoring.
|
|
|
|
## Definitions
|
|
|
|
Consistently with Go's terminology, we define
|
|
_HTTP round trip_ the process where we get a request
|
|
to send; we find a suitable connection for sending
|
|
it, or we create one; we send headers and
|
|
possibly body; and we receive response headers.
|
|
|
|
We also define _HTTP transaction_ the process starting
|
|
with an HTTP round trip and terminating by reading
|
|
the full response body.
|
|
|
|
We define _netx replacement_ a Go struct of interface that
|
|
has the same interface of a Go standard library object
|
|
but additionally performs measurements.
|
|
|
|
## Enhanced error handling
|
|
|
|
This library MUST wrap `error` such that:
|
|
|
|
1. we can classify all errors we care about; and
|
|
|
|
2. we can map them to major operations.
|
|
|
|
The `github.com/ooni/netx/modelx` MUST contain a wrapper for
|
|
Go `error` named `ErrWrapper` that is at least like:
|
|
|
|
```Go
|
|
type ErrWrapper struct {
|
|
Failure string // error classification
|
|
Operation string // operation that caused error
|
|
WrappedErr error // the original error
|
|
}
|
|
|
|
func (e *ErrWrapper) Error() string {
|
|
return e.Failure
|
|
}
|
|
```
|
|
|
|
Where `Failure` is one of the errors we care about, i.e.:
|
|
|
|
- `connection_refused`: ECONNREFUSED
|
|
- `connection_reset`: ECONNRESET
|
|
- `dns_bogon_error`: detected bogon in DNS reply
|
|
- `dns_nxdomain_error`: NXDOMAIN in DNS reply
|
|
- `eof_error`: unexpected EOF on connection
|
|
- `generic_timeout_error`: some timer has expired
|
|
- `ssl_invalid_hostname`: certificate not valid for SNI
|
|
- `ssl_unknown_authority`: cannot find CA validating certificate
|
|
- `ssl_invalid_certificate`: e.g. certificate expired
|
|
- `unknown_failure <string>`: any other error
|
|
|
|
Note that we care about bogons in DNS replies because they are
|
|
often used to censor specific websites.
|
|
|
|
And where `Operation` is one of:
|
|
|
|
- `resolve`: domain name resolution
|
|
- `connect`: TCP connect
|
|
- `tls_handshake`: TLS handshake
|
|
- `http_round_trip`: reading/writing HTTP
|
|
|
|
The code in this library MUST wrap returned errors such
|
|
that we can cast back to `ErrWrapper` during the analysis
|
|
phase, using Go 1.13 `errors` library as follows:
|
|
|
|
```Go
|
|
var wrapper *modelx.ErrWrapper
|
|
if errors.As(err, &wrapper) == true {
|
|
// Do something with the error
|
|
}
|
|
```
|
|
|
|
## Netx replacements
|
|
|
|
We want to provide netx replacements for the following
|
|
interfaces in the Go standard library:
|
|
|
|
1. `http.RoundTripper`
|
|
|
|
2. `http.Client`
|
|
|
|
3. `net.Dialer`
|
|
|
|
4. `net.Resolver`
|
|
|
|
Accordingly, we'll define the following interfaces in
|
|
the `github.com/ooni/probe-engine/netx/modelx` package:
|
|
|
|
```Go
|
|
type DNSResolver interface {
|
|
LookupHost(ctx context.Context, hostname string) ([]string, error)
|
|
}
|
|
|
|
type Dialer interface {
|
|
Dial(network, address string) (net.Conn, error)
|
|
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
|
}
|
|
|
|
type TLSDialer interface {
|
|
DialTLS(network, address string) (net.Conn, error)
|
|
DialTLSContext(ctx context.Context, network, address string) (net.Conn, error)
|
|
}
|
|
```
|
|
|
|
We won't need an interface for `http.RoundTripper`
|
|
because it is already an interface, so we'll just use it.
|
|
|
|
Our replacements will implement these interfaces.
|
|
|
|
Using an API compatible with Go's standard libary makes
|
|
it possible to use, say, our `net.Dialer` replacement with
|
|
other libraries. Both `http.Transport` and
|
|
`gorilla/websocket`'s `websocket.Dialer` have
|
|
functions like `Dial` and `DialContext` that can be
|
|
overriden. By overriding such function pointers,
|
|
we could use our replacements instead of the standard
|
|
libary, thus we could collect measurements while
|
|
using third party code to implement specific protocols.
|
|
|
|
Also, using interfaces allows us to combine code
|
|
quite easily. For example, a resolver that detects
|
|
bogons is easily implemented as a wrapper around
|
|
another resolve that performs the real resolution.
|
|
|
|
## Dispatching events
|
|
|
|
The `github.com/ooni/netx/modelx` package will define
|
|
an handler for low level events as:
|
|
|
|
```Go
|
|
type Handler interface {
|
|
OnMeasurement(Measurement)
|
|
}
|
|
```
|
|
|
|
We will provide a mechanism to bind a specific
|
|
handler to a `context.Context` such that the handler
|
|
will receive all the measurements caused by code
|
|
using such context. This mechanism is like:
|
|
|
|
```Go
|
|
type MeasurementRoot struct {
|
|
Beginning time.Time // the "zero" time
|
|
Handler Handler // the handler to use
|
|
}
|
|
```
|
|
|
|
You will be able to assign a `MeasurementRoot` to
|
|
a context by using the following function:
|
|
|
|
```Go
|
|
func WithMeasurementRoot(
|
|
ctx context.Context, root *MeasurementRoot) context.Context
|
|
```
|
|
|
|
which will return a clone of the original context
|
|
that uses the `MeasurementRoot`. Pass this context to
|
|
any method of our replacements to get measurements.
|
|
|
|
Given such context, or a subcontext, you can get
|
|
back the original `MeasurementRoot` using:
|
|
|
|
```Go
|
|
func ContextMeasurementRoot(ctx context.Context) *MeasurementRoot
|
|
```
|
|
|
|
which will return the context `MeasurementRoot` or
|
|
`nil` if none is set into the context. This is how our
|
|
internal code gets access to the `MeasurementRoot`.
|
|
|
|
## Constructing and configuring replacements
|
|
|
|
The `github.com/ooni/probe-engine/netx` package MUST provide an API such
|
|
that you can construct and configure a `net.Resolver` replacement
|
|
as follows:
|
|
|
|
```Go
|
|
r, err := netx.NewResolverWithoutHandler(dnsNetwork, dnsAddress)
|
|
if err != nil {
|
|
log.Fatal("cannot configure specifc resolver")
|
|
}
|
|
var resolver modelx.DNSResolver = r
|
|
// now use resolver ...
|
|
```
|
|
|
|
where `DNSNetwork` and `DNSAddress` configure the type
|
|
of the resolver as follows:
|
|
|
|
- when `DNSNetwork` is `""` or `"system"`, `DNSAddress` does
|
|
not matter and we use the system resolver
|
|
|
|
- when `DNSNetwork` is `"udp"`, `DNSAddress` is the address
|
|
or domain name, with optional port, of the DNS server
|
|
(e.g., `8.8.8.8:53`)
|
|
|
|
- when `DNSNetwork` is `"tcp"`, `DNSAddress` is the address
|
|
or domain name, with optional port, of the DNS server
|
|
(e.g., `8.8.8.8:53`)
|
|
|
|
- when `DNSNetwork` is `"dot"`, `DNSAddress` is the address
|
|
or domain name, with optional port, of the DNS server
|
|
(e.g., `8.8.8.8:853`)
|
|
|
|
- when `DNSNetwork` is `"doh"`, `DNSAddress` is the URL
|
|
of the DNS server (e.g. `https://cloudflare-dns.com/dns-query`)
|
|
|
|
When the resolve is not the system one, we'll also be able
|
|
to emit events when performing resolution. Otherwise, we'll
|
|
just emit the `DNSResolveDone` event defined below.
|
|
|
|
Any resolver returned by this function may be configured to return the
|
|
`dns_bogon_error` if any `LookupHost` lookup returns a bogon IP.
|
|
|
|
The package will also contain this function:
|
|
|
|
```Go
|
|
func ChainResolvers(
|
|
primary, secondary modelx.DNSResolver) modelx.DNSResolver
|
|
```
|
|
|
|
where you can create a new resolver where `secondary` will be
|
|
invoked whenever `primary` fails. This functionality allows
|
|
us to be more resilient and bypass automatically certain types
|
|
of censorship, e.g., a resolver returning a bogon.
|
|
|
|
The `github.com/ooni/probe-engine/netx` package MUST also provide an API such
|
|
that you can construct and configure a `net.Dialer` replacement
|
|
as follows:
|
|
|
|
```Go
|
|
d := netx.NewDialerWithoutHandler()
|
|
d.SetResolver(resolver)
|
|
d.ForceSpecificSNI("www.kernel.org")
|
|
d.SetCABundle("/etc/ssl/cert.pem")
|
|
d.ForceSkipVerify()
|
|
var dialer modelx.Dialer = d
|
|
// now use dialer
|
|
```
|
|
|
|
where `SetResolver` allows you to change the resolver,
|
|
`ForceSpecificSNI` forces the TLS dials to use such SNI
|
|
instead of using the provided domain, `SetCABundle`
|
|
allows to set a specific CA bundle, and `ForceSkipVerify`
|
|
allows to disable certificate verification. All these funcs
|
|
MUST NOT be invoked once you're using the dialer.
|
|
|
|
The `github.com/ooni/probe-engine/netx` package MUST contain
|
|
code so that we can do:
|
|
|
|
```Go
|
|
t := netx.NewHTTPTransportWithProxyFunc(
|
|
http.ProxyFromEnvironment,
|
|
)
|
|
t.SetResolver(resolver)
|
|
t.ForceSpecificSNI("www.kernel.org")
|
|
t.SetCABundle("/etc/ssl/cert.pem")
|
|
t.ForceSkipVerify()
|
|
var transport http.RoundTripper = t
|
|
// now use transport
|
|
```
|
|
|
|
where the functions have the same semantics as the
|
|
namesake functions described before and the same caveats.
|
|
|
|
We also have syntactic sugar on top of that and legacy
|
|
methods, but this fully describes the design.
|
|
|
|
## Structure of events
|
|
|
|
The `github.com/ooni/probe-engine/netx/modelx` will contain the
|
|
definition of low-level events. We are interested in
|
|
knowing the following:
|
|
|
|
1. the timing and result of each I/O operation.
|
|
|
|
2. the timing of HTTP events occurring during the
|
|
lifecycle of an HTTP request.
|
|
|
|
3. the timing and result of the TLS handshake including
|
|
the negotiated TLS version and other details such as
|
|
what certificates the server has provided.
|
|
|
|
4. DNS events, e.g. queries and replies, generated
|
|
as part of using DoT and DoH.
|
|
|
|
We will represent time as a `time.Duration` since the
|
|
beginning configured either in the context or when
|
|
constructing an object. The `modelx` package will also
|
|
define the `Measurement` event as follows:
|
|
|
|
```Go
|
|
type Measurement struct {
|
|
Connect *ConnectEvent
|
|
HTTPConnectionReady *HTTPConnectionReadyEvent
|
|
HTTPRoundTripDone *HTTPRoundTripDoneEvent
|
|
ResolveDone *ResolveDoneEvent
|
|
TLSHandshakeDone *TLSHandshakeDoneEvent
|
|
}
|
|
```
|
|
|
|
The events above MUST always be present, but more
|
|
events will likely be available. The structure
|
|
will contain a pointer for every event that
|
|
we support. The events processing code will check
|
|
what pointer or pointers are not `nil` to known
|
|
which event or events have occurred.
|
|
|
|
To simplify joining events together the following holds:
|
|
|
|
1. when we're establishing a new connection there is a nonzero
|
|
`DialID` shared by `Connect` and `ResolveDone`
|
|
|
|
2. a new connection has a nonzero `ConnID` that is emitted
|
|
as part of a successful `Connect` event
|
|
|
|
3. during an HTTP transaction there is a nonzero `TransactionID`
|
|
shared by `HTTPConnectionReady` and `HTTPRoundTripDone`
|
|
|
|
4. if the TLS handshake is invoked by HTTP code it will have a
|
|
nonzero `TrasactionID` otherwise a nonzero `ConnID`
|
|
|
|
5. the `HTTPConnectionReady` will also see the `ConnID`
|
|
|
|
6. when a transaction starts dialing, it will pass its
|
|
`TransactionID` to `ResolveDone` and `Connect`
|
|
|
|
7. when we're dialing a connection for DoH, we pass the `DialID`
|
|
to the `HTTPConnectionReady` event as well
|
|
|
|
Because of the following rules, it should always be possible
|
|
to bind together events. Also, we define more events than the
|
|
above, but they are ancillary to the above events. Also, the
|
|
main reason why `HTTPConnectionReady` is here is because it is
|
|
the event allowing to bind `ConnID` and `TransactionID`.
|