doc(measurex): explain how to write experiments (#529)
Part of https://github.com/ooni/ooni.org/issues/361 Co-authored-by: Arturo Filastò <arturo@openobservatory.org>
This commit is contained in:
parent
399d2f65da
commit
d45e58c14f
|
@ -10,6 +10,8 @@ real OONI code, it should always be up to date.
|
||||||
|
|
||||||
- [Rewriting the torsf experiment](experiment/torsf/)
|
- [Rewriting the torsf experiment](experiment/torsf/)
|
||||||
|
|
||||||
|
- [Using the measurex package to write network experiments](measurex)
|
||||||
|
|
||||||
|
|
||||||
## Regenerating the tutorials
|
## Regenerating the tutorials
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,25 @@ func gentorsf() {
|
||||||
gen(path.Join(prefix, "chapter04"), "torsf.go")
|
gen(path.Join(prefix, "chapter04"), "torsf.go")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// genmeasurex generates the measurex chapters.
|
||||||
|
func genmeasurex() {
|
||||||
|
prefix := path.Join(".", "measurex")
|
||||||
|
gen(path.Join(prefix, "chapter01"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter02"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter03"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter04"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter05"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter06"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter07"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter08"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter09"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter10"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter11"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter12"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter13"), "main.go")
|
||||||
|
gen(path.Join(prefix, "chapter14"), "main.go")
|
||||||
|
}
|
||||||
|
|
||||||
// gennetxlite generates the netxlite chapters.
|
// gennetxlite generates the netxlite chapters.
|
||||||
func gennetxlite() {
|
func gennetxlite() {
|
||||||
prefix := path.Join(".", "netxlite")
|
prefix := path.Join(".", "netxlite")
|
||||||
|
@ -102,7 +121,9 @@ func gennetxlite() {
|
||||||
gen(path.Join(prefix, "chapter07"), "main.go")
|
gen(path.Join(prefix, "chapter07"), "main.go")
|
||||||
gen(path.Join(prefix, "chapter08"), "main.go")
|
gen(path.Join(prefix, "chapter08"), "main.go")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
gentorsf()
|
gentorsf()
|
||||||
|
genmeasurex()
|
||||||
gennetxlite()
|
gennetxlite()
|
||||||
}
|
}
|
||||||
|
|
104
internal/tutorial/measurex/README.md
Normal file
104
internal/tutorial/measurex/README.md
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# Using the measurex package to write network experiments
|
||||||
|
|
||||||
|
This tutorial teaches you how to write OONI network
|
||||||
|
experiments using the primitives in the `./internal/measurex`
|
||||||
|
package. The name of this package means either "measure
|
||||||
|
extensions" or "measure crossover".
|
||||||
|
|
||||||
|
The measure extension interpretation of the name explains
|
||||||
|
what this package does. It contains extensions to our
|
||||||
|
basic networking code (`./internal/netxlite`) that allow
|
||||||
|
us to perform OONI measurements.
|
||||||
|
|
||||||
|
The measure crossover interpretation explains its history. Since
|
||||||
|
OONI has been written in Go, we've mostly performed measurements
|
||||||
|
using "tracing". That is, by registering hooks that run when
|
||||||
|
specific operations happen (e.g., TCP connect or TLS handshake)
|
||||||
|
and then making sense of the network trace. This is the approach
|
||||||
|
with which most experiments are written as of 2021-09-24. But
|
||||||
|
we have also seen that in several cases a step-by-step approach
|
||||||
|
is preferrable. Under this approach, you perform individual
|
||||||
|
operations and record their result right away. So, for example,
|
||||||
|
you have a connection and a TLS config, you perform a TLS
|
||||||
|
handshake, and immediately after you create and store somewhere
|
||||||
|
a data structure containing the result. This package is at the
|
||||||
|
crossover of these two approaches, because basically it contains
|
||||||
|
enough primitives to support both.
|
||||||
|
|
||||||
|
What we are going to do in this tutorial is the following:
|
||||||
|
|
||||||
|
- we will start from very simple-step-by-step measurements such
|
||||||
|
as TCP connect, DNS lookup, and TLS handshake;
|
||||||
|
|
||||||
|
- we will see how `measurex` provides support for composing
|
||||||
|
these primitives in larger steps, which will lead us to
|
||||||
|
eventually perform all the measurements that matter for a
|
||||||
|
given input URL (including discovering QUIC endpoints
|
||||||
|
and following redirections);
|
||||||
|
|
||||||
|
- finally, as an exercise, we will use the knowledge
|
||||||
|
acquired in the rest of the tutorial to rewrite a
|
||||||
|
subset of the Web Connectivity experiment (as of 2021-09-24
|
||||||
|
the flagship OONI experiment). This will be an opportunity
|
||||||
|
to explore more low level aspects of `measurex`.
|
||||||
|
|
||||||
|
As part of the process, we'll introduce you to the data
|
||||||
|
format used by OONI and there will be proposed exercises
|
||||||
|
where we simulate censorship conditions and we see how
|
||||||
|
that impacts the generated measurements.
|
||||||
|
|
||||||
|
Every chapter will show how to write a simple `main.go`
|
||||||
|
program that explains how to use some primitives. The
|
||||||
|
chapter text itself is autogenerated from comments inside
|
||||||
|
the actual `main.go` the we describe in the chapter.
|
||||||
|
|
||||||
|
For this reason, if you need to change a chapter, you
|
||||||
|
need to change the corresponding `main.go` file and then
|
||||||
|
follow the instructions at `./internal/tutorial/generate`
|
||||||
|
to regenerate the markdown text of the chapter.
|
||||||
|
|
||||||
|
More in detail, here's the index:
|
||||||
|
|
||||||
|
- [chapter01](chapter01) explains how to use the "system" resolver
|
||||||
|
|
||||||
|
- [chapter02](chapter02) deals with establishing TCP connections
|
||||||
|
|
||||||
|
- [chapter03](chapter03) is about using custom DNS-over-UDP resolvers
|
||||||
|
|
||||||
|
- [chapter04](chapter04) shows how to measure TLS handshakes
|
||||||
|
|
||||||
|
- [chapter05](chapter05) is about the QUIC handshake
|
||||||
|
|
||||||
|
- [chapter06](chapter06) shows how to get a webpage knowing its
|
||||||
|
URL and the endpoint (i.e., IP address and TCP/UDP port)
|
||||||
|
|
||||||
|
- [chapter07](chapter07) shows how to extend what we did in
|
||||||
|
chapter06 to cover _all_ the IP addresses in the URL's domain
|
||||||
|
|
||||||
|
- [chapter08](chapter08) is about HTTPSSvc DNS queries and
|
||||||
|
how they can be used to discover and test QUIC endpoints, thus
|
||||||
|
extending the work done in chapter07
|
||||||
|
|
||||||
|
- [chapter09](chapter09) improves upon chapter08 showing
|
||||||
|
how to run endpoints measurements in parallel
|
||||||
|
|
||||||
|
- [chapter10](chapter10) improves upon chapter09 by
|
||||||
|
also running DNS queries in parallel
|
||||||
|
|
||||||
|
- [chapter11](chapter11) tells you that all the code we
|
||||||
|
have been writing so far, and specifically the code we have
|
||||||
|
in chapter10, is actually the implementation of an API
|
||||||
|
of `measurex` called `MeasureURL`, and then shows you how
|
||||||
|
you can simplify the code in chapter10 by using this API.
|
||||||
|
|
||||||
|
- [chapter12](chapter12) extends the work done in
|
||||||
|
chapter11 by teaching you about a more high-level API
|
||||||
|
that discovers and follows all redirections, calling
|
||||||
|
`MeasureURL` for each redirection.
|
||||||
|
|
||||||
|
- [chapter13](chapter13) contains the exercise regarding
|
||||||
|
rewriting WebConnectivity using all the tools you have
|
||||||
|
learned so far and pointing you at additional `measurex`
|
||||||
|
API that could be useful to solve the problem.
|
||||||
|
|
||||||
|
- [chapter14](chapter14) contains our solution to the exercise.
|
366
internal/tutorial/measurex/chapter01/README.md
Normal file
366
internal/tutorial/measurex/chapter01/README.md
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
|
||||||
|
# Chapter I: using the system resolver
|
||||||
|
|
||||||
|
In this chapter we explain how to measure DNS resolutions performed
|
||||||
|
using the system resolver. *En passant*, we will also introduce you to
|
||||||
|
the `Measurer`, which we will use for the rest of the tutorial.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter01/main.go`.)
|
||||||
|
|
||||||
|
## The system resolver
|
||||||
|
|
||||||
|
We define "system resolver" as the DNS resolver implemented by the C
|
||||||
|
library. On Unix, the most popular interface to such a resolver is
|
||||||
|
the `getaddrinfo(3)` C library function.
|
||||||
|
|
||||||
|
Most OONI experiments (also known as nettests) use the system
|
||||||
|
resolver to map domain names to IP addresses. The advantage of
|
||||||
|
the system resolver is that it's provided by the system. So,
|
||||||
|
it should _generally_ work. Also, it is the resolver that the
|
||||||
|
user of the system will use every day, therefore its results
|
||||||
|
should be representative (even though the rise of DNS over
|
||||||
|
HTTPS embedded in browsers may make this statement less solid
|
||||||
|
than it were ten years ago).
|
||||||
|
|
||||||
|
The disadvantage of the system resolver is that we do not
|
||||||
|
know how it is configured. Say the user has configured a
|
||||||
|
DNS over TLS resolver; then the measurements may miss censorship
|
||||||
|
that we would otherwise see if using a custom DNS resolver.
|
||||||
|
|
||||||
|
Now that we have justified why the system resolver is
|
||||||
|
important for OONI, let us perform some measurements with it.
|
||||||
|
|
||||||
|
We will first write a simple `main.go` file that shows how to use
|
||||||
|
this functionality. Then, we will show some runs of this file, and
|
||||||
|
we will comment the output that we see.
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
We declare the package and import useful packages. The most
|
||||||
|
important package we're importing here is, of course, `internal/measurex`.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
```
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
We define command line flags useful to test this program. We use
|
||||||
|
the `flags` package for that. We want the user to be able to configure
|
||||||
|
both the domain name to resolve and the resolution timeout.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
domain := flag.String("domain", "example.com", "domain to resolve")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
```
|
||||||
|
|
||||||
|
We call `flag.Parse` to parse the CLI flags.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
flag.Parse()
|
||||||
|
```
|
||||||
|
|
||||||
|
We create a context and we attach a timeout to it. (This is a pretty
|
||||||
|
standard way of configuring a timeout in Go.)
|
||||||
|
|
||||||
|
```Go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Measurer
|
||||||
|
|
||||||
|
Now we create a `Measurer`.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Measurer` is a concrete type that contains many fields
|
||||||
|
requiring initialization. For this reason, we provide a factory
|
||||||
|
that creates one with default settings. The expected usage
|
||||||
|
pattern is that you do not modify a `Measurer`'s field after
|
||||||
|
initialization. Modifying them while the `Measurer` is in
|
||||||
|
use could, in fact, lead to races.
|
||||||
|
|
||||||
|
Let's now invoke the system resolver to resolve `*domain`!
|
||||||
|
|
||||||
|
### Invoking the system resolver
|
||||||
|
|
||||||
|
We call the `LookupHostSystem` method of the `Measurer`. The
|
||||||
|
arguments are the Context, that in this case carries the timeout
|
||||||
|
we configured above, and the domain to resolve.
|
||||||
|
|
||||||
|
The call itself is named `LookupHost` because this is the name
|
||||||
|
used by the Go function that performs a domain lookup.
|
||||||
|
|
||||||
|
Under the hood, `mx.LookupHostSystem` will eventually call
|
||||||
|
`(*net.Resolver).LookupHost`. In turn, in the common case on
|
||||||
|
Unix, this function will eventually call `getaddrinfo(3)`.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m := mx.LookupHostSystem(ctx, *domain)
|
||||||
|
```
|
||||||
|
|
||||||
|
The return value of `(*net.Resolver).LookupHost` is either a
|
||||||
|
list of IP addresses or an error. Our `LookupHostSystem` method,
|
||||||
|
instead, returns a `*measurex.DNSMeasurement` type.
|
||||||
|
|
||||||
|
This is probably a good moment to remind you of Go's
|
||||||
|
built in help system. We could include a definition of the
|
||||||
|
`DNSMeasurement` structure, but since this definition is
|
||||||
|
just a comment in the main.go file, it might age badly.
|
||||||
|
|
||||||
|
Instead, if you run
|
||||||
|
|
||||||
|
```
|
||||||
|
go doc ./internal/measurex.DNSMeasurement
|
||||||
|
```
|
||||||
|
|
||||||
|
You get the current definition. As you can see, this type
|
||||||
|
is basically just a wrapper around `Measurement`. Now,
|
||||||
|
checking the docs of `Measurement` with
|
||||||
|
|
||||||
|
```
|
||||||
|
go doc ./internal/measurex.Measurement
|
||||||
|
```
|
||||||
|
|
||||||
|
we can see a container of events
|
||||||
|
classified by event type. In our case, because we're
|
||||||
|
doing a `LookupHost`, we should have at least one entry
|
||||||
|
inside of the `Measurement.LookupHost` field.
|
||||||
|
|
||||||
|
This entry is of type `DNSLookupEvent`. Let us check
|
||||||
|
together the definition of this type:
|
||||||
|
|
||||||
|
```
|
||||||
|
go doc ./internal/measurex.DNSLookupEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are familiar with [the OONI data format specs](
|
||||||
|
https://github.com/ooni/spec/tree/master/data-formats), you
|
||||||
|
should probably recognize that this structure is the Go
|
||||||
|
representation of the `df-002-dnst` data format.
|
||||||
|
|
||||||
|
In fact, every event field inside of a `Measurement`
|
||||||
|
should serialize nicely to JSON to one of the OONI data
|
||||||
|
formats.
|
||||||
|
|
||||||
|
### Printing the measurement
|
||||||
|
|
||||||
|
Because there is a close relationship between the
|
||||||
|
events inside a `Measurement` and the JSON OONI data
|
||||||
|
format, in the remainder of this program we're
|
||||||
|
going to serialize the `Measurement` to JSON and
|
||||||
|
print it to the standard output.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
```
|
||||||
|
|
||||||
|
As a final note, the `PanicOnError` is here because the
|
||||||
|
message `m` *can* be marshalled to JSON. It still feels a
|
||||||
|
bit better having an assertion for our assumptions than
|
||||||
|
outrightly ignoring the error code. (We tend to use such
|
||||||
|
a convention quite frequently in the OONI codebase.)
|
||||||
|
|
||||||
|
```Go
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us run the program with default arguments first. You can do
|
||||||
|
this operation by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter01
|
||||||
|
```
|
||||||
|
|
||||||
|
If you do that you obtain some logging messages, which are out of
|
||||||
|
the scope of this tutorial, and the following JSON:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
"lookup_host": [
|
||||||
|
{
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"answer_type": "A",
|
||||||
|
"ipv4": "93.184.216.34"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engine": "system",
|
||||||
|
"failure": null,
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "A",
|
||||||
|
"resolver_address": "",
|
||||||
|
"t": 0.002996459,
|
||||||
|
"started": 9.8e-05,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"answer_type": "AAAA",
|
||||||
|
"ivp6": "2606:2800:220:1:248:1893:25c8:1946"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engine": "system",
|
||||||
|
"failure": null,
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "AAAA",
|
||||||
|
"resolver_address": "",
|
||||||
|
"t": 0.002996459,
|
||||||
|
"started": 9.8e-05,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You see that we have two messages here. OONI splits a DNS
|
||||||
|
resolution performed using the system resolver into two "fake"
|
||||||
|
DNS resolutions for A and AAAA. (Under the hood, this is
|
||||||
|
what the system resolver would most likely do.)
|
||||||
|
|
||||||
|
The most important fields are:
|
||||||
|
|
||||||
|
- _engine_, indicating that we are using the "system" resolver;
|
||||||
|
|
||||||
|
- _hostname_, meaning that we wanted to resolve the "example.com" domain;
|
||||||
|
|
||||||
|
- _answers_, which contains a list of answers;
|
||||||
|
|
||||||
|
- _t_, which is the time when the LookupHost operation completed.
|
||||||
|
|
||||||
|
### NXDOMAIN measurement
|
||||||
|
|
||||||
|
Let us now change the domain to resolve to be `antani.ooni.org` (a
|
||||||
|
nonexisting domain), which we can do by running this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter01 -domain antani.ooni.org
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the output JSON:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"domain": "antani.ooni.org",
|
||||||
|
"lookup_host": [
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "system",
|
||||||
|
"failure": "dns_nxdomain_error",
|
||||||
|
"hostname": "antani.ooni.org",
|
||||||
|
"query_type": "A",
|
||||||
|
"resolver_address": "",
|
||||||
|
"t": 0.072963834,
|
||||||
|
"started": 0.000125417,
|
||||||
|
"oddity": "dns.lookup.nxdomain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "system",
|
||||||
|
"failure": "dns_nxdomain_error",
|
||||||
|
"hostname": "antani.ooni.org",
|
||||||
|
"query_type": "AAAA",
|
||||||
|
"resolver_address": "",
|
||||||
|
"t": 0.072963834,
|
||||||
|
"started": 0.000125417,
|
||||||
|
"oddity": "dns.lookup.nxdomain"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So we see a failure that says there was indeed an NXDOMAIN
|
||||||
|
error and we also see a field named `oddity`.
|
||||||
|
|
||||||
|
What is an oddity? We define oddity something unexpected thay
|
||||||
|
may be explained by censorship as well as by a transient failure
|
||||||
|
or other normal network conditions. (In this case, the result
|
||||||
|
is perfectly normal since we're looking up a nonexistent domain.)
|
||||||
|
|
||||||
|
The difference between failure and oddity is that the failure
|
||||||
|
indicates the error that occurred, while the oddity classifies
|
||||||
|
the error in the context of the operation during which it
|
||||||
|
occurred. (In this case the difference is subtle, but we'll
|
||||||
|
have a better example later, when we'll see what happens on timeout.)
|
||||||
|
|
||||||
|
Failures are specified in
|
||||||
|
[df-007-errors](https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md).
|
||||||
|
Inside the `internal/netxlite/errorsx`
|
||||||
|
package, there is code that maps Go errors to failures. (The
|
||||||
|
`netxlite` package is the fundamental network package we use, on
|
||||||
|
top of which `measurex` is written.)
|
||||||
|
|
||||||
|
### Measurement with timeout
|
||||||
|
|
||||||
|
Let us now try with an insanely low timeout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter01 -timeout 250us
|
||||||
|
```
|
||||||
|
|
||||||
|
To get this JSON:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
"lookup_host": [
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "system",
|
||||||
|
"failure": "generic_timeout_error",
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "A",
|
||||||
|
"resolver_address": "",
|
||||||
|
"t": 0.000489167,
|
||||||
|
"started": 9.2583e-05,
|
||||||
|
"oddity": "dns.lookup.timeout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "system",
|
||||||
|
"failure": "generic_timeout_error",
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "AAAA",
|
||||||
|
"resolver_address": "",
|
||||||
|
"t": 0.000489167,
|
||||||
|
"started": 9.2583e-05,
|
||||||
|
"oddity": "dns.lookup.timeout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You should now better see the difference between a failure and
|
||||||
|
an oddity. The context timeout maps to a `generic_timeout_error` while
|
||||||
|
the oddity clearly indicates the timeout happens during a DNS
|
||||||
|
lookup. As we mentioned above, the failure is just an error while
|
||||||
|
an oddity is an error put in context.
|
||||||
|
|
||||||
|
## Conclusions
|
||||||
|
|
||||||
|
This is it. We have seen how to measure with the system resolver and we have
|
||||||
|
also seen which easy-to-provoke errors we can get.
|
||||||
|
|
368
internal/tutorial/measurex/chapter01/main.go
Normal file
368
internal/tutorial/measurex/chapter01/main.go
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter I: using the system resolver
|
||||||
|
//
|
||||||
|
// In this chapter we explain how to measure DNS resolutions performed
|
||||||
|
// using the system resolver. *En passant*, we will also introduce you to
|
||||||
|
// the `Measurer`, which we will use for the rest of the tutorial.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter01/main.go`.)
|
||||||
|
//
|
||||||
|
// ## The system resolver
|
||||||
|
//
|
||||||
|
// We define "system resolver" as the DNS resolver implemented by the C
|
||||||
|
// library. On Unix, the most popular interface to such a resolver is
|
||||||
|
// the `getaddrinfo(3)` C library function.
|
||||||
|
//
|
||||||
|
// Most OONI experiments (also known as nettests) use the system
|
||||||
|
// resolver to map domain names to IP addresses. The advantage of
|
||||||
|
// the system resolver is that it's provided by the system. So,
|
||||||
|
// it should _generally_ work. Also, it is the resolver that the
|
||||||
|
// user of the system will use every day, therefore its results
|
||||||
|
// should be representative (even though the rise of DNS over
|
||||||
|
// HTTPS embedded in browsers may make this statement less solid
|
||||||
|
// than it were ten years ago).
|
||||||
|
//
|
||||||
|
// The disadvantage of the system resolver is that we do not
|
||||||
|
// know how it is configured. Say the user has configured a
|
||||||
|
// DNS over TLS resolver; then the measurements may miss censorship
|
||||||
|
// that we would otherwise see if using a custom DNS resolver.
|
||||||
|
//
|
||||||
|
// Now that we have justified why the system resolver is
|
||||||
|
// important for OONI, let us perform some measurements with it.
|
||||||
|
//
|
||||||
|
// We will first write a simple `main.go` file that shows how to use
|
||||||
|
// this functionality. Then, we will show some runs of this file, and
|
||||||
|
// we will comment the output that we see.
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// We declare the package and import useful packages. The most
|
||||||
|
// important package we're importing here is, of course, `internal/measurex`.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// ```
|
||||||
|
// ### Setup
|
||||||
|
//
|
||||||
|
// We define command line flags useful to test this program. We use
|
||||||
|
// the `flags` package for that. We want the user to be able to configure
|
||||||
|
// both the domain name to resolve and the resolution timeout.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
domain := flag.String("domain", "example.com", "domain to resolve")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We call `flag.Parse` to parse the CLI flags.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
flag.Parse()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We create a context and we attach a timeout to it. (This is a pretty
|
||||||
|
// standard way of configuring a timeout in Go.)
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Creating a Measurer
|
||||||
|
//
|
||||||
|
// Now we create a `Measurer`.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The `Measurer` is a concrete type that contains many fields
|
||||||
|
// requiring initialization. For this reason, we provide a factory
|
||||||
|
// that creates one with default settings. The expected usage
|
||||||
|
// pattern is that you do not modify a `Measurer`'s field after
|
||||||
|
// initialization. Modifying them while the `Measurer` is in
|
||||||
|
// use could, in fact, lead to races.
|
||||||
|
//
|
||||||
|
// Let's now invoke the system resolver to resolve `*domain`!
|
||||||
|
//
|
||||||
|
// ### Invoking the system resolver
|
||||||
|
//
|
||||||
|
// We call the `LookupHostSystem` method of the `Measurer`. The
|
||||||
|
// arguments are the Context, that in this case carries the timeout
|
||||||
|
// we configured above, and the domain to resolve.
|
||||||
|
//
|
||||||
|
// The call itself is named `LookupHost` because this is the name
|
||||||
|
// used by the Go function that performs a domain lookup.
|
||||||
|
//
|
||||||
|
// Under the hood, `mx.LookupHostSystem` will eventually call
|
||||||
|
// `(*net.Resolver).LookupHost`. In turn, in the common case on
|
||||||
|
// Unix, this function will eventually call `getaddrinfo(3)`.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m := mx.LookupHostSystem(ctx, *domain)
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The return value of `(*net.Resolver).LookupHost` is either a
|
||||||
|
// list of IP addresses or an error. Our `LookupHostSystem` method,
|
||||||
|
// instead, returns a `*measurex.DNSMeasurement` type.
|
||||||
|
//
|
||||||
|
// This is probably a good moment to remind you of Go's
|
||||||
|
// built in help system. We could include a definition of the
|
||||||
|
// `DNSMeasurement` structure, but since this definition is
|
||||||
|
// just a comment in the main.go file, it might age badly.
|
||||||
|
//
|
||||||
|
// Instead, if you run
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go doc ./internal/measurex.DNSMeasurement
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// You get the current definition. As you can see, this type
|
||||||
|
// is basically just a wrapper around `Measurement`. Now,
|
||||||
|
// checking the docs of `Measurement` with
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go doc ./internal/measurex.Measurement
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// we can see a container of events
|
||||||
|
// classified by event type. In our case, because we're
|
||||||
|
// doing a `LookupHost`, we should have at least one entry
|
||||||
|
// inside of the `Measurement.LookupHost` field.
|
||||||
|
//
|
||||||
|
// This entry is of type `DNSLookupEvent`. Let us check
|
||||||
|
// together the definition of this type:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go doc ./internal/measurex.DNSLookupEvent
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// If you are familiar with [the OONI data format specs](
|
||||||
|
// https://github.com/ooni/spec/tree/master/data-formats), you
|
||||||
|
// should probably recognize that this structure is the Go
|
||||||
|
// representation of the `df-002-dnst` data format.
|
||||||
|
//
|
||||||
|
// In fact, every event field inside of a `Measurement`
|
||||||
|
// should serialize nicely to JSON to one of the OONI data
|
||||||
|
// formats.
|
||||||
|
//
|
||||||
|
// ### Printing the measurement
|
||||||
|
//
|
||||||
|
// Because there is a close relationship between the
|
||||||
|
// events inside a `Measurement` and the JSON OONI data
|
||||||
|
// format, in the remainder of this program we're
|
||||||
|
// going to serialize the `Measurement` to JSON and
|
||||||
|
// print it to the standard output.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// As a final note, the `PanicOnError` is here because the
|
||||||
|
// message `m` *can* be marshalled to JSON. It still feels a
|
||||||
|
// bit better having an assertion for our assumptions than
|
||||||
|
// outrightly ignoring the error code. (We tend to use such
|
||||||
|
// a convention quite frequently in the OONI codebase.)
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us run the program with default arguments first. You can do
|
||||||
|
// this operation by running:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter01
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// If you do that you obtain some logging messages, which are out of
|
||||||
|
// the scope of this tutorial, and the following JSON:
|
||||||
|
//
|
||||||
|
// ```JSON
|
||||||
|
// {
|
||||||
|
// "domain": "example.com",
|
||||||
|
// "lookup_host": [
|
||||||
|
// {
|
||||||
|
// "answers": [
|
||||||
|
// {
|
||||||
|
// "answer_type": "A",
|
||||||
|
// "ipv4": "93.184.216.34"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "engine": "system",
|
||||||
|
// "failure": null,
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "A",
|
||||||
|
// "resolver_address": "",
|
||||||
|
// "t": 0.002996459,
|
||||||
|
// "started": 9.8e-05,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "answers": [
|
||||||
|
// {
|
||||||
|
// "answer_type": "AAAA",
|
||||||
|
// "ivp6": "2606:2800:220:1:248:1893:25c8:1946"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "engine": "system",
|
||||||
|
// "failure": null,
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "AAAA",
|
||||||
|
// "resolver_address": "",
|
||||||
|
// "t": 0.002996459,
|
||||||
|
// "started": 9.8e-05,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// You see that we have two messages here. OONI splits a DNS
|
||||||
|
// resolution performed using the system resolver into two "fake"
|
||||||
|
// DNS resolutions for A and AAAA. (Under the hood, this is
|
||||||
|
// what the system resolver would most likely do.)
|
||||||
|
//
|
||||||
|
// The most important fields are:
|
||||||
|
//
|
||||||
|
// - _engine_, indicating that we are using the "system" resolver;
|
||||||
|
//
|
||||||
|
// - _hostname_, meaning that we wanted to resolve the "example.com" domain;
|
||||||
|
//
|
||||||
|
// - _answers_, which contains a list of answers;
|
||||||
|
//
|
||||||
|
// - _t_, which is the time when the LookupHost operation completed.
|
||||||
|
//
|
||||||
|
// ### NXDOMAIN measurement
|
||||||
|
//
|
||||||
|
// Let us now change the domain to resolve to be `antani.ooni.org` (a
|
||||||
|
// nonexisting domain), which we can do by running this command:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter01 -domain antani.ooni.org
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is the output JSON:
|
||||||
|
//
|
||||||
|
// ```JSON
|
||||||
|
// {
|
||||||
|
// "domain": "antani.ooni.org",
|
||||||
|
// "lookup_host": [
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "system",
|
||||||
|
// "failure": "dns_nxdomain_error",
|
||||||
|
// "hostname": "antani.ooni.org",
|
||||||
|
// "query_type": "A",
|
||||||
|
// "resolver_address": "",
|
||||||
|
// "t": 0.072963834,
|
||||||
|
// "started": 0.000125417,
|
||||||
|
// "oddity": "dns.lookup.nxdomain"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "system",
|
||||||
|
// "failure": "dns_nxdomain_error",
|
||||||
|
// "hostname": "antani.ooni.org",
|
||||||
|
// "query_type": "AAAA",
|
||||||
|
// "resolver_address": "",
|
||||||
|
// "t": 0.072963834,
|
||||||
|
// "started": 0.000125417,
|
||||||
|
// "oddity": "dns.lookup.nxdomain"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// So we see a failure that says there was indeed an NXDOMAIN
|
||||||
|
// error and we also see a field named `oddity`.
|
||||||
|
//
|
||||||
|
// What is an oddity? We define oddity something unexpected thay
|
||||||
|
// may be explained by censorship as well as by a transient failure
|
||||||
|
// or other normal network conditions. (In this case, the result
|
||||||
|
// is perfectly normal since we're looking up a nonexistent domain.)
|
||||||
|
//
|
||||||
|
// The difference between failure and oddity is that the failure
|
||||||
|
// indicates the error that occurred, while the oddity classifies
|
||||||
|
// the error in the context of the operation during which it
|
||||||
|
// occurred. (In this case the difference is subtle, but we'll
|
||||||
|
// have a better example later, when we'll see what happens on timeout.)
|
||||||
|
//
|
||||||
|
// Failures are specified in
|
||||||
|
// [df-007-errors](https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md).
|
||||||
|
// Inside the `internal/netxlite/errorsx`
|
||||||
|
// package, there is code that maps Go errors to failures. (The
|
||||||
|
// `netxlite` package is the fundamental network package we use, on
|
||||||
|
// top of which `measurex` is written.)
|
||||||
|
//
|
||||||
|
// ### Measurement with timeout
|
||||||
|
//
|
||||||
|
// Let us now try with an insanely low timeout:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter01 -timeout 250us
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// To get this JSON:
|
||||||
|
//
|
||||||
|
// ```JSON
|
||||||
|
// {
|
||||||
|
// "domain": "example.com",
|
||||||
|
// "lookup_host": [
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "system",
|
||||||
|
// "failure": "generic_timeout_error",
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "A",
|
||||||
|
// "resolver_address": "",
|
||||||
|
// "t": 0.000489167,
|
||||||
|
// "started": 9.2583e-05,
|
||||||
|
// "oddity": "dns.lookup.timeout"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "system",
|
||||||
|
// "failure": "generic_timeout_error",
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "AAAA",
|
||||||
|
// "resolver_address": "",
|
||||||
|
// "t": 0.000489167,
|
||||||
|
// "started": 9.2583e-05,
|
||||||
|
// "oddity": "dns.lookup.timeout"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// You should now better see the difference between a failure and
|
||||||
|
// an oddity. The context timeout maps to a `generic_timeout_error` while
|
||||||
|
// the oddity clearly indicates the timeout happens during a DNS
|
||||||
|
// lookup. As we mentioned above, the failure is just an error while
|
||||||
|
// an oddity is an error put in context.
|
||||||
|
//
|
||||||
|
// ## Conclusions
|
||||||
|
//
|
||||||
|
// This is it. We have seen how to measure with the system resolver and we have
|
||||||
|
// also seen which easy-to-provoke errors we can get.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
228
internal/tutorial/measurex/chapter02/README.md
Normal file
228
internal/tutorial/measurex/chapter02/README.md
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
|
||||||
|
# Chapter II: establishing TCP connections
|
||||||
|
|
||||||
|
In this chapter we explain how to measure establishing TCP connections.
|
||||||
|
|
||||||
|
We will first write a simple `main.go` file that shows how to use
|
||||||
|
this functionality. Then, we will show some runs of this file, and
|
||||||
|
we will comment the output that we see.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter02/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
We declare the package and import useful packages. The most
|
||||||
|
important package we're importing here is, of course, `internal/measurex`.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
```
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
This first part of `main.go` is really similar to the previous
|
||||||
|
chapter, so there is not much new to say here.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
address := flag.String("address", "8.8.4.4:443", "remote endpoint address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Measurer
|
||||||
|
|
||||||
|
We create a `Measurer` like we did in the previous chapter.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Establishing a TCP connection
|
||||||
|
|
||||||
|
We then call `TCPConnect`, which establishes a connection
|
||||||
|
and returns the corresponding measurement.
|
||||||
|
|
||||||
|
The arguments are the context (for timeouts), and the address
|
||||||
|
of the endpoint to which we want to connect. (Here and in
|
||||||
|
most of this tutorial with "endpoint" we mean an IP address
|
||||||
|
and a port, serialized as "ADDRESS:PORT", where the
|
||||||
|
address is quoted with "[" and "]" if IPv6, e.g., `[::1]:53`.)
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m := mx.TCPConnect(ctx, *address)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Printing the measurement
|
||||||
|
|
||||||
|
The rest of the main function is just like in the previous chapter.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us run the program with default arguments first. You can do
|
||||||
|
this operation by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter02
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is the JSON we obtain in output:
|
||||||
|
|
||||||
|
```JavaScript
|
||||||
|
{
|
||||||
|
// These two fields identify the endpoint
|
||||||
|
"network": "tcp",
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
|
||||||
|
// This block contains the results of the connect syscall
|
||||||
|
// using the df-008-netevents data format.
|
||||||
|
"connect": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"operation": "connect",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.026879041,
|
||||||
|
"started": 8.8625e-05,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is what it says:
|
||||||
|
|
||||||
|
- we are connecting a "tcp" socket;
|
||||||
|
|
||||||
|
- the destination endpoint address is "8.8.4.4:443";
|
||||||
|
|
||||||
|
- connect terminated ~0.027 seconds into the program's life;
|
||||||
|
|
||||||
|
- the operation succeeded (`failure` is `nil`).
|
||||||
|
|
||||||
|
Let us now see if we can provoke some errors and timeouts.
|
||||||
|
|
||||||
|
### Measurement with connection refused
|
||||||
|
|
||||||
|
Let us start with an IP address where there's no listening socket:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter02 -address 127.0.0.1:1
|
||||||
|
```
|
||||||
|
|
||||||
|
We get this JSON:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"network": "tcp",
|
||||||
|
"address": "127.0.0.1:1",
|
||||||
|
"connect": [
|
||||||
|
{
|
||||||
|
"address": "127.0.0.1:1",
|
||||||
|
"failure": "connection_refused",
|
||||||
|
"operation": "connect",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.000372167,
|
||||||
|
"started": 8.4917e-05,
|
||||||
|
"oddity": "tcp.connect.refused"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
And here's an error telling us the connection was refused and
|
||||||
|
the oddity that classifies the error.
|
||||||
|
|
||||||
|
### Measurement with timeouts
|
||||||
|
|
||||||
|
Let us now try to obtain a timeout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter02 -address 8.8.4.4:1
|
||||||
|
```
|
||||||
|
|
||||||
|
We get this JSON:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"network": "tcp",
|
||||||
|
"address": "8.8.4.4:1",
|
||||||
|
"connect": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:1",
|
||||||
|
"failure": "generic_timeout_error",
|
||||||
|
"operation": "connect",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 10.005494583,
|
||||||
|
"started": 8.4833e-05,
|
||||||
|
"oddity": "tcp.connect.timeout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So, we clearly see from the value of `t` that our 60 seconds
|
||||||
|
default timeout did not hit, because there is a lower watchdog
|
||||||
|
timeout (10 s). We also see again how the oddity is more
|
||||||
|
precise than just the error alone.
|
||||||
|
|
||||||
|
Let us now use a very small timeout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter02 -address 8.8.4.4:1 -timeout 100ms
|
||||||
|
```
|
||||||
|
|
||||||
|
To get this JSON:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"network": "tcp",
|
||||||
|
"address": "8.8.4.4:1",
|
||||||
|
"connect": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:1",
|
||||||
|
"failure": "generic_timeout_error",
|
||||||
|
"operation": "connect",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.10148025,
|
||||||
|
"started": 0.000122375,
|
||||||
|
"oddity": "tcp.connect.timeout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We see a timeout after ~0.1s. We enforce a reasonably small
|
||||||
|
timeout for connecting, equal to 10 s, because we want to
|
||||||
|
guarantee that measurements eventually terminate. Also, since
|
||||||
|
often censorship is implemented by timing out, we don't want
|
||||||
|
to spend to much time waiting for a timeout to expire.
|
||||||
|
|
||||||
|
## Conclusions
|
||||||
|
|
||||||
|
We have seen how to measure the operation of connecting
|
||||||
|
to a specific TCP endpoint.
|
||||||
|
|
230
internal/tutorial/measurex/chapter02/main.go
Normal file
230
internal/tutorial/measurex/chapter02/main.go
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter II: establishing TCP connections
|
||||||
|
//
|
||||||
|
// In this chapter we explain how to measure establishing TCP connections.
|
||||||
|
//
|
||||||
|
// We will first write a simple `main.go` file that shows how to use
|
||||||
|
// this functionality. Then, we will show some runs of this file, and
|
||||||
|
// we will comment the output that we see.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter02/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// We declare the package and import useful packages. The most
|
||||||
|
// important package we're importing here is, of course, `internal/measurex`.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// ```
|
||||||
|
// ### Setup
|
||||||
|
//
|
||||||
|
// This first part of `main.go` is really similar to the previous
|
||||||
|
// chapter, so there is not much new to say here.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
address := flag.String("address", "8.8.4.4:443", "remote endpoint address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Creating a Measurer
|
||||||
|
//
|
||||||
|
// We create a `Measurer` like we did in the previous chapter.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Establishing a TCP connection
|
||||||
|
//
|
||||||
|
// We then call `TCPConnect`, which establishes a connection
|
||||||
|
// and returns the corresponding measurement.
|
||||||
|
//
|
||||||
|
// The arguments are the context (for timeouts), and the address
|
||||||
|
// of the endpoint to which we want to connect. (Here and in
|
||||||
|
// most of this tutorial with "endpoint" we mean an IP address
|
||||||
|
// and a port, serialized as "ADDRESS:PORT", where the
|
||||||
|
// address is quoted with "[" and "]" if IPv6, e.g., `[::1]:53`.)
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m := mx.TCPConnect(ctx, *address)
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Printing the measurement
|
||||||
|
//
|
||||||
|
// The rest of the main function is just like in the previous chapter.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us run the program with default arguments first. You can do
|
||||||
|
// this operation by running:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter02
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Here is the JSON we obtain in output:
|
||||||
|
//
|
||||||
|
// ```JavaScript
|
||||||
|
// {
|
||||||
|
// // These two fields identify the endpoint
|
||||||
|
// "network": "tcp",
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
//
|
||||||
|
// // This block contains the results of the connect syscall
|
||||||
|
// // using the df-008-netevents data format.
|
||||||
|
// "connect": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "operation": "connect",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.026879041,
|
||||||
|
// "started": 8.8625e-05,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is what it says:
|
||||||
|
//
|
||||||
|
// - we are connecting a "tcp" socket;
|
||||||
|
//
|
||||||
|
// - the destination endpoint address is "8.8.4.4:443";
|
||||||
|
//
|
||||||
|
// - connect terminated ~0.027 seconds into the program's life;
|
||||||
|
//
|
||||||
|
// - the operation succeeded (`failure` is `nil`).
|
||||||
|
//
|
||||||
|
// Let us now see if we can provoke some errors and timeouts.
|
||||||
|
//
|
||||||
|
// ### Measurement with connection refused
|
||||||
|
//
|
||||||
|
// Let us start with an IP address where there's no listening socket:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter02 -address 127.0.0.1:1
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We get this JSON:
|
||||||
|
//
|
||||||
|
// ```JSON
|
||||||
|
// {
|
||||||
|
// "network": "tcp",
|
||||||
|
// "address": "127.0.0.1:1",
|
||||||
|
// "connect": [
|
||||||
|
// {
|
||||||
|
// "address": "127.0.0.1:1",
|
||||||
|
// "failure": "connection_refused",
|
||||||
|
// "operation": "connect",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.000372167,
|
||||||
|
// "started": 8.4917e-05,
|
||||||
|
// "oddity": "tcp.connect.refused"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// And here's an error telling us the connection was refused and
|
||||||
|
// the oddity that classifies the error.
|
||||||
|
//
|
||||||
|
// ### Measurement with timeouts
|
||||||
|
//
|
||||||
|
// Let us now try to obtain a timeout:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter02 -address 8.8.4.4:1
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We get this JSON:
|
||||||
|
//
|
||||||
|
// ```JSON
|
||||||
|
// {
|
||||||
|
// "network": "tcp",
|
||||||
|
// "address": "8.8.4.4:1",
|
||||||
|
// "connect": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:1",
|
||||||
|
// "failure": "generic_timeout_error",
|
||||||
|
// "operation": "connect",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 10.005494583,
|
||||||
|
// "started": 8.4833e-05,
|
||||||
|
// "oddity": "tcp.connect.timeout"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// So, we clearly see from the value of `t` that our 60 seconds
|
||||||
|
// default timeout did not hit, because there is a lower watchdog
|
||||||
|
// timeout (10 s). We also see again how the oddity is more
|
||||||
|
// precise than just the error alone.
|
||||||
|
//
|
||||||
|
// Let us now use a very small timeout:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter02 -address 8.8.4.4:1 -timeout 100ms
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// To get this JSON:
|
||||||
|
//
|
||||||
|
// ```JSON
|
||||||
|
// {
|
||||||
|
// "network": "tcp",
|
||||||
|
// "address": "8.8.4.4:1",
|
||||||
|
// "connect": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:1",
|
||||||
|
// "failure": "generic_timeout_error",
|
||||||
|
// "operation": "connect",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.10148025,
|
||||||
|
// "started": 0.000122375,
|
||||||
|
// "oddity": "tcp.connect.timeout"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We see a timeout after ~0.1s. We enforce a reasonably small
|
||||||
|
// timeout for connecting, equal to 10 s, because we want to
|
||||||
|
// guarantee that measurements eventually terminate. Also, since
|
||||||
|
// often censorship is implemented by timing out, we don't want
|
||||||
|
// to spend to much time waiting for a timeout to expire.
|
||||||
|
//
|
||||||
|
// ## Conclusions
|
||||||
|
//
|
||||||
|
// We have seen how to measure the operation of connecting
|
||||||
|
// to a specific TCP endpoint.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
567
internal/tutorial/measurex/chapter03/README.md
Normal file
567
internal/tutorial/measurex/chapter03/README.md
Normal file
|
@ -0,0 +1,567 @@
|
||||||
|
|
||||||
|
# Chapter III: using a custom DNS-over-UDP resolver
|
||||||
|
|
||||||
|
In this chapter we learn how to measure sending DNS queries to
|
||||||
|
a DNS server speaking the DNS-over-UDP protocol.
|
||||||
|
|
||||||
|
Without further ado, let's describe our example `main.go` program
|
||||||
|
and let's use it to better understand this flow.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter03/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The initial part of the program is pretty much the same as the one
|
||||||
|
used in previous chapters, so I will not add further comments.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
query := flag.String("query", "example.com", "domain to resolver")
|
||||||
|
address := flag.String("address", "8.8.4.4:53", "DNS-over-UDP server address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using a custom UDP resolver
|
||||||
|
|
||||||
|
We now invoke `LookupHostUDP`. We specify:
|
||||||
|
|
||||||
|
- a context for timeout information;
|
||||||
|
|
||||||
|
- the domain to query for;
|
||||||
|
|
||||||
|
- the address of the DNS-over-UDP server endpoint.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m := mx.LookupHostUDP(ctx, *query, *address)
|
||||||
|
```
|
||||||
|
|
||||||
|
Also this operation returns a measurement, which
|
||||||
|
we print using the usual three-liner.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
As before, let us start off with a vanilla run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter03
|
||||||
|
```
|
||||||
|
|
||||||
|
This time we get a much larger JSON, so I will pretend it is
|
||||||
|
actually JavaScript and add comments to explain it inline.
|
||||||
|
|
||||||
|
(This is the first case in which we see how a single
|
||||||
|
method call for measurer causes several events to
|
||||||
|
be generated and inserted into a `Measurement`.)
|
||||||
|
|
||||||
|
```JavaScript
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
|
||||||
|
// This block tells us about the UDP connect events
|
||||||
|
// where we bind to the server's endpoint
|
||||||
|
"connect": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:53",
|
||||||
|
"failure": null,
|
||||||
|
"operation": "connect",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.00043175,
|
||||||
|
"started": 0.000191958,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:53",
|
||||||
|
"failure": null,
|
||||||
|
"operation": "connect",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.042198458,
|
||||||
|
"started": 0.042113208,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// This block shows the read and write events
|
||||||
|
// occurred on the sockets (because we control
|
||||||
|
// in full the implementation of this DNS
|
||||||
|
// over UDP resolver, we can see these events)
|
||||||
|
"read_write": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 29,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.000459583,
|
||||||
|
"started": 0.00043825,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 45,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.041955792,
|
||||||
|
"started": 0.000471833,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 29,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.042218917,
|
||||||
|
"started": 0.042203,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 57,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.196646583,
|
||||||
|
"started": 0.042233167,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// This is the same kind of result as before, we
|
||||||
|
// show the emitted queries and the resolved addrs.
|
||||||
|
//
|
||||||
|
// Also note how here the resolver_address is the
|
||||||
|
// correct endpoint address and the engine tells us
|
||||||
|
// that we're using DNS over UDP.
|
||||||
|
"lookup_host": [
|
||||||
|
{
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"answer_type": "A",
|
||||||
|
"ipv4": "93.184.216.34"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engine": "udp",
|
||||||
|
"failure": null,
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "A",
|
||||||
|
"resolver_address": "8.8.4.4:53",
|
||||||
|
"t": 0.196777042,
|
||||||
|
"started": 0.000118542,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"answer_type": "AAAA",
|
||||||
|
"ivp6": "2606:2800:220:1:248:1893:25c8:1946"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engine": "udp",
|
||||||
|
"failure": null,
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "AAAA",
|
||||||
|
"resolver_address": "8.8.4.4:53",
|
||||||
|
"t": 0.196777042,
|
||||||
|
"started": 0.000118542,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// This block shows the query we sent (encoded as base64)
|
||||||
|
// and the response we received. Here we clearly see
|
||||||
|
// that we perform two DNS "round trip" (i.e., send request
|
||||||
|
// and receive response) to resolve a domain: one for
|
||||||
|
// A and the other for AAAA.
|
||||||
|
"dns_round_trip": [
|
||||||
|
{
|
||||||
|
"engine": "udp",
|
||||||
|
"resolver_address": "8.8.4.4:53",
|
||||||
|
"raw_query": {
|
||||||
|
"data": "PrcBAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
"started": 0.000191625,
|
||||||
|
"t": 0.041998667,
|
||||||
|
"failure": null,
|
||||||
|
"raw_reply": {
|
||||||
|
"data": "PreBgAABAAEAAAAAB2V4YW1wbGUDY29tAAABAAHADAABAAEAAE8BAARduNgi",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"engine": "udp",
|
||||||
|
"resolver_address": "8.8.4.4:53",
|
||||||
|
"raw_query": {
|
||||||
|
"data": "LAwBAAABAAAAAAAAB2V4YW1wbGUDY29tAAAcAAE=",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
"started": 0.04210775,
|
||||||
|
"t": 0.196701333,
|
||||||
|
"failure": null,
|
||||||
|
"raw_reply": {
|
||||||
|
"data": "LAyBgAABAAEAAAAAB2V4YW1wbGUDY29tAAAcAAHADAAcAAEAAE6nABAmBigAAiAAAQJIGJMlyBlG",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This data format is really an extension of the `LookupHostSystem`
|
||||||
|
one. It just adds more fields that clarify what happened at low
|
||||||
|
level in terms of socket I/O and queries sent and received.
|
||||||
|
|
||||||
|
Let us now try to provoke some errors and see how the
|
||||||
|
output JSON changes because of them.
|
||||||
|
|
||||||
|
### Measurement with NXDOMAIN
|
||||||
|
|
||||||
|
Let us try to get a NXDOMAIN error.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter03 -query antani.ooni.org
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces the following JSON:
|
||||||
|
|
||||||
|
```JavaScript
|
||||||
|
{
|
||||||
|
"domain": "antani.ooni.org",
|
||||||
|
"connect": [ /* snip */ ],
|
||||||
|
"read_write": [ /* snip */ ],
|
||||||
|
"lookup_host": [
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "udp",
|
||||||
|
"failure": "dns_nxdomain_error",
|
||||||
|
"hostname": "antani.ooni.org",
|
||||||
|
"query_type": "A",
|
||||||
|
"resolver_address": "8.8.4.4:53",
|
||||||
|
"t": 0.098208709,
|
||||||
|
"started": 8.95e-05,
|
||||||
|
"oddity": "dns.lookup.nxdomain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "udp",
|
||||||
|
"failure": "dns_nxdomain_error",
|
||||||
|
"hostname": "antani.ooni.org",
|
||||||
|
"query_type": "AAAA",
|
||||||
|
"resolver_address": "8.8.4.4:53",
|
||||||
|
"t": 0.098208709,
|
||||||
|
"started": 8.95e-05,
|
||||||
|
"oddity": "dns.lookup.nxdomain"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dns_round_trip": [
|
||||||
|
{
|
||||||
|
"engine": "udp",
|
||||||
|
"resolver_address": "8.8.4.4:53",
|
||||||
|
"raw_query": {
|
||||||
|
"data": "jLIBAAABAAAAAAAABmFudGFuaQRvb25pA29yZwAAAQAB",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
"started": 0.000141542,
|
||||||
|
"t": 0.034689417,
|
||||||
|
"failure": null,
|
||||||
|
"raw_reply": {
|
||||||
|
"data": "jLKBgwABAAAAAQAABmFudGFuaQRvb25pA29yZwAAAQABwBMABgABAAAHCAA9BGRuczERcmVnaXN0cmFyLXNlcnZlcnMDY29tAApob3N0bWFzdGVywDJhABz8AACowAAADhAACTqAAAAOEQ==",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"engine": "udp",
|
||||||
|
"resolver_address": "8.8.4.4:53",
|
||||||
|
"raw_query": {
|
||||||
|
"data": "azEBAAABAAAAAAAABmFudGFuaQRvb25pA29yZwAAHAAB",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
"started": 0.034776709,
|
||||||
|
"t": 0.098170542,
|
||||||
|
"failure": null,
|
||||||
|
"raw_reply": {
|
||||||
|
"data": "azGBgwABAAAAAQAABmFudGFuaQRvb25pA29yZwAAHAABwBMABgABAAAHCAA9BGRuczERcmVnaXN0cmFyLXNlcnZlcnMDY29tAApob3N0bWFzdGVywDJhABz8AACowAAADhAACTqAAAAOEQ==",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We indeed get a NXDOMAIN error as the failure in `lookup_host`.
|
||||||
|
|
||||||
|
Let us now decode one of the replies by using this program:
|
||||||
|
|
||||||
|
```
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const query = "azGBgwABAAAAAQAABmFudGFuaQRvb25pA29yZwAAHAABwBMABgABAAAHCAA9BGRuczERcmVnaXN0cmFyLXNlcnZlcnMDY29tAApob3N0bWFzdGVywDJhABz8AACowAAADhAACTqAAAAOEQ=="
|
||||||
|
data, _ := base64.StdEncoding.DecodeString(query)
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
_ = msg.Unpack(data)
|
||||||
|
fmt.Printf("%s\n", msg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
where `query` is one of the replies. If we run this program
|
||||||
|
we get as the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
;; opcode: QUERY, status: NXDOMAIN, id: 27441
|
||||||
|
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0
|
||||||
|
|
||||||
|
;; QUESTION SECTION:
|
||||||
|
;antani.ooni.org. IN AAAA
|
||||||
|
|
||||||
|
;; AUTHORITY SECTION:
|
||||||
|
ooni.org. 1800 IN SOA dns1.registrar-servers.com. hostmaster.registrar-servers.com. 1627397372 43200 3600 604800 3601
|
||||||
|
```
|
||||||
|
|
||||||
|
### Measurement with timeout
|
||||||
|
|
||||||
|
Let us now query an IP address known for not responding
|
||||||
|
to DNS queries, to get a timeout.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter03 -address 182.92.22.222:53
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's the corresponding JSON:
|
||||||
|
|
||||||
|
```JavaScript
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
"connect": [ /* snip */ ],
|
||||||
|
"read_write": [
|
||||||
|
{
|
||||||
|
"address": "182.92.22.222:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 29,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.0005275,
|
||||||
|
"started": 0.000500209,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "182.92.22.222:53",
|
||||||
|
"failure": "generic_timeout_error", /* <--- */
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 5.001140125,
|
||||||
|
"started": 0.000544042,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lookup_host": [
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "udp",
|
||||||
|
"failure": "generic_timeout_error", /* <--- */
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "A",
|
||||||
|
"resolver_address": "182.92.22.222:53",
|
||||||
|
"t": 5.001462084,
|
||||||
|
"started": 0.000127917,
|
||||||
|
"oddity": "dns.lookup.timeout" /* <--- */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "udp",
|
||||||
|
"failure": "generic_timeout_error",
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "AAAA",
|
||||||
|
"resolver_address": "182.92.22.222:53",
|
||||||
|
"t": 5.001462084,
|
||||||
|
"started": 0.000127917,
|
||||||
|
"oddity": "dns.lookup.timeout"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dns_round_trip": [
|
||||||
|
{
|
||||||
|
"engine": "udp",
|
||||||
|
"resolver_address": "182.92.22.222:53",
|
||||||
|
"raw_query": {
|
||||||
|
"data": "ej8BAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
"started": 0.000220584,
|
||||||
|
"t": 5.001317417,
|
||||||
|
"failure": "generic_timeout_error",
|
||||||
|
"raw_reply": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We see that we do fail with a timeout (I have marked some of them
|
||||||
|
with comments inside the JSON). We see the timeout at three different
|
||||||
|
level of abstractions (from lower to higher abstraction): at the socket layer,
|
||||||
|
during the DNS round trip, during the DNS lookup.
|
||||||
|
|
||||||
|
What we also see is that `t`'s value is ~5s when the `read` event
|
||||||
|
fails, which tells us about the socket's read timeout.
|
||||||
|
|
||||||
|
### Measurement with REFUSED error
|
||||||
|
|
||||||
|
Let us now try to get a REFUSED DNS Rcode, again from servers
|
||||||
|
that are, let's say, kind enough to easily help.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter03 -address 180.97.36.63:53
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's the answer I get:
|
||||||
|
|
||||||
|
```JavaScript
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
"connect": [ /* snip */ ],
|
||||||
|
|
||||||
|
// The I/O events look normal this time
|
||||||
|
"read_write": [
|
||||||
|
{
|
||||||
|
"address": "180.97.36.63:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 29,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.000333583,
|
||||||
|
"started": 0.000312125,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "180.97.36.63:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 29,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.334948125,
|
||||||
|
"started": 0.000366625,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "180.97.36.63:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 29,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.3358025,
|
||||||
|
"started": 0.335725958,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "180.97.36.63:53",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 29,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "udp",
|
||||||
|
"t": 0.739987666,
|
||||||
|
"started": 0.335863875,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// But we see both in the error and in the oddity
|
||||||
|
// that the response was "REFUSED"
|
||||||
|
"lookup_host": [
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "udp",
|
||||||
|
"failure": "dns_refused_error",
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "A",
|
||||||
|
"resolver_address": "180.97.36.63:53",
|
||||||
|
"t": 0.7402975,
|
||||||
|
"started": 7.2291e-05,
|
||||||
|
"oddity": "dns.lookup.refused"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": null,
|
||||||
|
"engine": "udp",
|
||||||
|
"failure": "dns_refused_error",
|
||||||
|
"hostname": "example.com",
|
||||||
|
"query_type": "AAAA",
|
||||||
|
"resolver_address": "180.97.36.63:53",
|
||||||
|
"t": 0.7402975,
|
||||||
|
"started": 7.2291e-05,
|
||||||
|
"oddity": "dns.lookup.refused"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Exercise: do like I did before and decode the messages
|
||||||
|
"dns_round_trip": [
|
||||||
|
{
|
||||||
|
"engine": "udp",
|
||||||
|
"resolver_address": "180.97.36.63:53",
|
||||||
|
"raw_query": {
|
||||||
|
"data": "crkBAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
"started": 0.000130666,
|
||||||
|
"t": 0.33509925,
|
||||||
|
"failure": null,
|
||||||
|
"raw_reply": {
|
||||||
|
"data": "crmBBQABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"engine": "udp",
|
||||||
|
"resolver_address": "180.97.36.63:53",
|
||||||
|
"raw_query": {
|
||||||
|
"data": "ywcBAAABAAAAAAAAB2V4YW1wbGUDY29tAAAcAAE=",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
"started": 0.335321333,
|
||||||
|
"t": 0.740152375,
|
||||||
|
"failure": null,
|
||||||
|
"raw_reply": {
|
||||||
|
"data": "yweBBQABAAAAAAAAB2V4YW1wbGUDY29tAAAcAAE=",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have seen how we sending DNS queries over UDP, measure the
|
||||||
|
results, and what happens on common error conditions.
|
||||||
|
|
569
internal/tutorial/measurex/chapter03/main.go
Normal file
569
internal/tutorial/measurex/chapter03/main.go
Normal file
|
@ -0,0 +1,569 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter III: using a custom DNS-over-UDP resolver
|
||||||
|
//
|
||||||
|
// In this chapter we learn how to measure sending DNS queries to
|
||||||
|
// a DNS server speaking the DNS-over-UDP protocol.
|
||||||
|
//
|
||||||
|
// Without further ado, let's describe our example `main.go` program
|
||||||
|
// and let's use it to better understand this flow.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter03/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The initial part of the program is pretty much the same as the one
|
||||||
|
// used in previous chapters, so I will not add further comments.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
query := flag.String("query", "example.com", "domain to resolver")
|
||||||
|
address := flag.String("address", "8.8.4.4:53", "DNS-over-UDP server address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Using a custom UDP resolver
|
||||||
|
//
|
||||||
|
// We now invoke `LookupHostUDP`. We specify:
|
||||||
|
//
|
||||||
|
// - a context for timeout information;
|
||||||
|
//
|
||||||
|
// - the domain to query for;
|
||||||
|
//
|
||||||
|
// - the address of the DNS-over-UDP server endpoint.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m := mx.LookupHostUDP(ctx, *query, *address)
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Also this operation returns a measurement, which
|
||||||
|
// we print using the usual three-liner.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// As before, let us start off with a vanilla run:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter03
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This time we get a much larger JSON, so I will pretend it is
|
||||||
|
// actually JavaScript and add comments to explain it inline.
|
||||||
|
//
|
||||||
|
// (This is the first case in which we see how a single
|
||||||
|
// method call for measurer causes several events to
|
||||||
|
// be generated and inserted into a `Measurement`.)
|
||||||
|
//
|
||||||
|
// ```JavaScript
|
||||||
|
// {
|
||||||
|
// "domain": "example.com",
|
||||||
|
//
|
||||||
|
// // This block tells us about the UDP connect events
|
||||||
|
// // where we bind to the server's endpoint
|
||||||
|
// "connect": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "operation": "connect",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.00043175,
|
||||||
|
// "started": 0.000191958,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "operation": "connect",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.042198458,
|
||||||
|
// "started": 0.042113208,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // This block shows the read and write events
|
||||||
|
// // occurred on the sockets (because we control
|
||||||
|
// // in full the implementation of this DNS
|
||||||
|
// // over UDP resolver, we can see these events)
|
||||||
|
// "read_write": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 29,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.000459583,
|
||||||
|
// "started": 0.00043825,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 45,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.041955792,
|
||||||
|
// "started": 0.000471833,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 29,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.042218917,
|
||||||
|
// "started": 0.042203,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 57,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.196646583,
|
||||||
|
// "started": 0.042233167,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // This is the same kind of result as before, we
|
||||||
|
// // show the emitted queries and the resolved addrs.
|
||||||
|
// //
|
||||||
|
// // Also note how here the resolver_address is the
|
||||||
|
// // correct endpoint address and the engine tells us
|
||||||
|
// // that we're using DNS over UDP.
|
||||||
|
// "lookup_host": [
|
||||||
|
// {
|
||||||
|
// "answers": [
|
||||||
|
// {
|
||||||
|
// "answer_type": "A",
|
||||||
|
// "ipv4": "93.184.216.34"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "engine": "udp",
|
||||||
|
// "failure": null,
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "A",
|
||||||
|
// "resolver_address": "8.8.4.4:53",
|
||||||
|
// "t": 0.196777042,
|
||||||
|
// "started": 0.000118542,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "answers": [
|
||||||
|
// {
|
||||||
|
// "answer_type": "AAAA",
|
||||||
|
// "ivp6": "2606:2800:220:1:248:1893:25c8:1946"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "engine": "udp",
|
||||||
|
// "failure": null,
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "AAAA",
|
||||||
|
// "resolver_address": "8.8.4.4:53",
|
||||||
|
// "t": 0.196777042,
|
||||||
|
// "started": 0.000118542,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // This block shows the query we sent (encoded as base64)
|
||||||
|
// // and the response we received. Here we clearly see
|
||||||
|
// // that we perform two DNS "round trip" (i.e., send request
|
||||||
|
// // and receive response) to resolve a domain: one for
|
||||||
|
// // A and the other for AAAA.
|
||||||
|
// "dns_round_trip": [
|
||||||
|
// {
|
||||||
|
// "engine": "udp",
|
||||||
|
// "resolver_address": "8.8.4.4:53",
|
||||||
|
// "raw_query": {
|
||||||
|
// "data": "PrcBAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// "started": 0.000191625,
|
||||||
|
// "t": 0.041998667,
|
||||||
|
// "failure": null,
|
||||||
|
// "raw_reply": {
|
||||||
|
// "data": "PreBgAABAAEAAAAAB2V4YW1wbGUDY29tAAABAAHADAABAAEAAE8BAARduNgi",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "engine": "udp",
|
||||||
|
// "resolver_address": "8.8.4.4:53",
|
||||||
|
// "raw_query": {
|
||||||
|
// "data": "LAwBAAABAAAAAAAAB2V4YW1wbGUDY29tAAAcAAE=",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// "started": 0.04210775,
|
||||||
|
// "t": 0.196701333,
|
||||||
|
// "failure": null,
|
||||||
|
// "raw_reply": {
|
||||||
|
// "data": "LAyBgAABAAEAAAAAB2V4YW1wbGUDY29tAAAcAAHADAAcAAEAAE6nABAmBigAAiAAAQJIGJMlyBlG",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This data format is really an extension of the `LookupHostSystem`
|
||||||
|
// one. It just adds more fields that clarify what happened at low
|
||||||
|
// level in terms of socket I/O and queries sent and received.
|
||||||
|
//
|
||||||
|
// Let us now try to provoke some errors and see how the
|
||||||
|
// output JSON changes because of them.
|
||||||
|
//
|
||||||
|
// ### Measurement with NXDOMAIN
|
||||||
|
//
|
||||||
|
// Let us try to get a NXDOMAIN error.
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter03 -query antani.ooni.org
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This produces the following JSON:
|
||||||
|
//
|
||||||
|
// ```JavaScript
|
||||||
|
// {
|
||||||
|
// "domain": "antani.ooni.org",
|
||||||
|
// "connect": [ /* snip */ ],
|
||||||
|
// "read_write": [ /* snip */ ],
|
||||||
|
// "lookup_host": [
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "udp",
|
||||||
|
// "failure": "dns_nxdomain_error",
|
||||||
|
// "hostname": "antani.ooni.org",
|
||||||
|
// "query_type": "A",
|
||||||
|
// "resolver_address": "8.8.4.4:53",
|
||||||
|
// "t": 0.098208709,
|
||||||
|
// "started": 8.95e-05,
|
||||||
|
// "oddity": "dns.lookup.nxdomain"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "udp",
|
||||||
|
// "failure": "dns_nxdomain_error",
|
||||||
|
// "hostname": "antani.ooni.org",
|
||||||
|
// "query_type": "AAAA",
|
||||||
|
// "resolver_address": "8.8.4.4:53",
|
||||||
|
// "t": 0.098208709,
|
||||||
|
// "started": 8.95e-05,
|
||||||
|
// "oddity": "dns.lookup.nxdomain"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "dns_round_trip": [
|
||||||
|
// {
|
||||||
|
// "engine": "udp",
|
||||||
|
// "resolver_address": "8.8.4.4:53",
|
||||||
|
// "raw_query": {
|
||||||
|
// "data": "jLIBAAABAAAAAAAABmFudGFuaQRvb25pA29yZwAAAQAB",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// "started": 0.000141542,
|
||||||
|
// "t": 0.034689417,
|
||||||
|
// "failure": null,
|
||||||
|
// "raw_reply": {
|
||||||
|
// "data": "jLKBgwABAAAAAQAABmFudGFuaQRvb25pA29yZwAAAQABwBMABgABAAAHCAA9BGRuczERcmVnaXN0cmFyLXNlcnZlcnMDY29tAApob3N0bWFzdGVywDJhABz8AACowAAADhAACTqAAAAOEQ==",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "engine": "udp",
|
||||||
|
// "resolver_address": "8.8.4.4:53",
|
||||||
|
// "raw_query": {
|
||||||
|
// "data": "azEBAAABAAAAAAAABmFudGFuaQRvb25pA29yZwAAHAAB",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// "started": 0.034776709,
|
||||||
|
// "t": 0.098170542,
|
||||||
|
// "failure": null,
|
||||||
|
// "raw_reply": {
|
||||||
|
// "data": "azGBgwABAAAAAQAABmFudGFuaQRvb25pA29yZwAAHAABwBMABgABAAAHCAA9BGRuczERcmVnaXN0cmFyLXNlcnZlcnMDY29tAApob3N0bWFzdGVywDJhABz8AACowAAADhAACTqAAAAOEQ==",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We indeed get a NXDOMAIN error as the failure in `lookup_host`.
|
||||||
|
//
|
||||||
|
// Let us now decode one of the replies by using this program:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// package main
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "fmt"
|
||||||
|
// "encoding/base64"
|
||||||
|
//
|
||||||
|
// "github.com/miekg/dns"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// const query = "azGBgwABAAAAAQAABmFudGFuaQRvb25pA29yZwAAHAABwBMABgABAAAHCAA9BGRuczERcmVnaXN0cmFyLXNlcnZlcnMDY29tAApob3N0bWFzdGVywDJhABz8AACowAAADhAACTqAAAAOEQ=="
|
||||||
|
// data, _ := base64.StdEncoding.DecodeString(query)
|
||||||
|
// msg := new(dns.Msg)
|
||||||
|
// _ = msg.Unpack(data)
|
||||||
|
// fmt.Printf("%s\n", msg)
|
||||||
|
//}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// where `query` is one of the replies. If we run this program
|
||||||
|
// we get as the output:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// ;; opcode: QUERY, status: NXDOMAIN, id: 27441
|
||||||
|
// ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0
|
||||||
|
//
|
||||||
|
// ;; QUESTION SECTION:
|
||||||
|
// ;antani.ooni.org. IN AAAA
|
||||||
|
//
|
||||||
|
// ;; AUTHORITY SECTION:
|
||||||
|
// ooni.org. 1800 IN SOA dns1.registrar-servers.com. hostmaster.registrar-servers.com. 1627397372 43200 3600 604800 3601
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Measurement with timeout
|
||||||
|
//
|
||||||
|
// Let us now query an IP address known for not responding
|
||||||
|
// to DNS queries, to get a timeout.
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter03 -address 182.92.22.222:53
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Here's the corresponding JSON:
|
||||||
|
//
|
||||||
|
// ```JavaScript
|
||||||
|
// {
|
||||||
|
// "domain": "example.com",
|
||||||
|
// "connect": [ /* snip */ ],
|
||||||
|
// "read_write": [
|
||||||
|
// {
|
||||||
|
// "address": "182.92.22.222:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 29,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.0005275,
|
||||||
|
// "started": 0.000500209,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "182.92.22.222:53",
|
||||||
|
// "failure": "generic_timeout_error", /* <--- */
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 5.001140125,
|
||||||
|
// "started": 0.000544042,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "lookup_host": [
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "udp",
|
||||||
|
// "failure": "generic_timeout_error", /* <--- */
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "A",
|
||||||
|
// "resolver_address": "182.92.22.222:53",
|
||||||
|
// "t": 5.001462084,
|
||||||
|
// "started": 0.000127917,
|
||||||
|
// "oddity": "dns.lookup.timeout" /* <--- */
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "udp",
|
||||||
|
// "failure": "generic_timeout_error",
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "AAAA",
|
||||||
|
// "resolver_address": "182.92.22.222:53",
|
||||||
|
// "t": 5.001462084,
|
||||||
|
// "started": 0.000127917,
|
||||||
|
// "oddity": "dns.lookup.timeout"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "dns_round_trip": [
|
||||||
|
// {
|
||||||
|
// "engine": "udp",
|
||||||
|
// "resolver_address": "182.92.22.222:53",
|
||||||
|
// "raw_query": {
|
||||||
|
// "data": "ej8BAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// "started": 0.000220584,
|
||||||
|
// "t": 5.001317417,
|
||||||
|
// "failure": "generic_timeout_error",
|
||||||
|
// "raw_reply": null
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We see that we do fail with a timeout (I have marked some of them
|
||||||
|
// with comments inside the JSON). We see the timeout at three different
|
||||||
|
// level of abstractions (from lower to higher abstraction): at the socket layer,
|
||||||
|
// during the DNS round trip, during the DNS lookup.
|
||||||
|
//
|
||||||
|
// What we also see is that `t`'s value is ~5s when the `read` event
|
||||||
|
// fails, which tells us about the socket's read timeout.
|
||||||
|
//
|
||||||
|
// ### Measurement with REFUSED error
|
||||||
|
//
|
||||||
|
// Let us now try to get a REFUSED DNS Rcode, again from servers
|
||||||
|
// that are, let's say, kind enough to easily help.
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter03 -address 180.97.36.63:53
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Here's the answer I get:
|
||||||
|
//
|
||||||
|
// ```JavaScript
|
||||||
|
// {
|
||||||
|
// "domain": "example.com",
|
||||||
|
// "connect": [ /* snip */ ],
|
||||||
|
//
|
||||||
|
// // The I/O events look normal this time
|
||||||
|
// "read_write": [
|
||||||
|
// {
|
||||||
|
// "address": "180.97.36.63:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 29,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.000333583,
|
||||||
|
// "started": 0.000312125,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "180.97.36.63:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 29,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.334948125,
|
||||||
|
// "started": 0.000366625,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "180.97.36.63:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 29,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.3358025,
|
||||||
|
// "started": 0.335725958,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "180.97.36.63:53",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 29,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "udp",
|
||||||
|
// "t": 0.739987666,
|
||||||
|
// "started": 0.335863875,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // But we see both in the error and in the oddity
|
||||||
|
// // that the response was "REFUSED"
|
||||||
|
// "lookup_host": [
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "udp",
|
||||||
|
// "failure": "dns_refused_error",
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "A",
|
||||||
|
// "resolver_address": "180.97.36.63:53",
|
||||||
|
// "t": 0.7402975,
|
||||||
|
// "started": 7.2291e-05,
|
||||||
|
// "oddity": "dns.lookup.refused"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "answers": null,
|
||||||
|
// "engine": "udp",
|
||||||
|
// "failure": "dns_refused_error",
|
||||||
|
// "hostname": "example.com",
|
||||||
|
// "query_type": "AAAA",
|
||||||
|
// "resolver_address": "180.97.36.63:53",
|
||||||
|
// "t": 0.7402975,
|
||||||
|
// "started": 7.2291e-05,
|
||||||
|
// "oddity": "dns.lookup.refused"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // Exercise: do like I did before and decode the messages
|
||||||
|
// "dns_round_trip": [
|
||||||
|
// {
|
||||||
|
// "engine": "udp",
|
||||||
|
// "resolver_address": "180.97.36.63:53",
|
||||||
|
// "raw_query": {
|
||||||
|
// "data": "crkBAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// "started": 0.000130666,
|
||||||
|
// "t": 0.33509925,
|
||||||
|
// "failure": null,
|
||||||
|
// "raw_reply": {
|
||||||
|
// "data": "crmBBQABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "engine": "udp",
|
||||||
|
// "resolver_address": "180.97.36.63:53",
|
||||||
|
// "raw_query": {
|
||||||
|
// "data": "ywcBAAABAAAAAAAAB2V4YW1wbGUDY29tAAAcAAE=",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// "started": 0.335321333,
|
||||||
|
// "t": 0.740152375,
|
||||||
|
// "failure": null,
|
||||||
|
// "raw_reply": {
|
||||||
|
// "data": "yweBBQABAAAAAAAAB2V4YW1wbGUDY29tAAAcAAE=",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have seen how we sending DNS queries over UDP, measure the
|
||||||
|
// results, and what happens on common error conditions.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
247
internal/tutorial/measurex/chapter04/README.md
Normal file
247
internal/tutorial/measurex/chapter04/README.md
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
|
||||||
|
# Chapter IV: TLS handshaking
|
||||||
|
|
||||||
|
This chapter describes measuring TLS handshakes.
|
||||||
|
|
||||||
|
Without further ado, let's describe our example `main.go` program
|
||||||
|
and let's use it to better understand how to measure that.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter04/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The initial part of the program is pretty much the same as the one
|
||||||
|
used in previous chapters, so I will not add further comments.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sni := flag.String("sni", "dns.google", "domain to resolver")
|
||||||
|
address := flag.String("address", "8.8.4.4:443", "remote endpoint address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connecting and handshaking.
|
||||||
|
|
||||||
|
We call the `ConnectAndHandshake` method. The arguments
|
||||||
|
are the context, the address, and a TLS config.
|
||||||
|
|
||||||
|
Under the hood, the code will call the TCP connect functionality
|
||||||
|
we have seen in chapter02, using the address argument. Then, if
|
||||||
|
successful, it will TLS handshake using the given TLS config.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m := mx.TLSConnectAndHandshake(ctx, *address, &tls.Config{
|
||||||
|
ServerName: *sni,
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
RootCAs: netxlite.NewDefaultCertPool(),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Regarding the TLS config, in particular,
|
||||||
|
the three fields above are the files you should always set
|
||||||
|
in a TLS config when doing handshakes manually. The `ServerName`
|
||||||
|
field forces the SNI, the NextProtos field forces the ALPN,
|
||||||
|
and the `RootCAs` field is overridden so that we use the
|
||||||
|
CA bundle that we bundle with OONI. (This CA bundle is the
|
||||||
|
same you can find at https://curl.haxx.se/ca/.)
|
||||||
|
|
||||||
|
As usual, the method to perform a measurement returns
|
||||||
|
the measurement itself, which we print below.
|
||||||
|
|
||||||
|
```
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
As before, let us start off with a vanilla run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter04
|
||||||
|
```
|
||||||
|
|
||||||
|
Let us comment the JSON in detail:
|
||||||
|
|
||||||
|
```JavaScript
|
||||||
|
{
|
||||||
|
"network": "tcp",
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
|
||||||
|
// This block is generated when connecting to a TCP
|
||||||
|
// socket, as we've already seen in chapter02
|
||||||
|
"connect": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"operation": "connect",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.046959084,
|
||||||
|
"started": 0.022998875,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// These are the I/O events during the handshake
|
||||||
|
"read_write": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 280,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.048752875,
|
||||||
|
"started": 0.04874125,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 517,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.087221334,
|
||||||
|
"started": 0.048760417,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 4301,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.088843584,
|
||||||
|
"started": 0.088830959,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 64,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.092078042,
|
||||||
|
"started": 0.092064042,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// This block contains information about the handshake
|
||||||
|
"tls_handshake": [
|
||||||
|
{
|
||||||
|
"cipher_suite": "TLS_AES_128_GCM_SHA256",
|
||||||
|
"failure": null,
|
||||||
|
"negotiated_proto": "h2",
|
||||||
|
"tls_version": "TLSv1.3",
|
||||||
|
"peer_certificates": [
|
||||||
|
{
|
||||||
|
"data": "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",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAwMDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzpkgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsXlOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcmBA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKAgOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwLtmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYDVR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYGCCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcwAoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcGA1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3BraS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcNAQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQcSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrLRklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U+o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2YrPxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IERlQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGsYye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjOz23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJGAJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKwjuDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UECxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYxOTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwSiV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351kKSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZDrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zkj5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esWCruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35EiEua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbapsZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAfBgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUHMAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6AloCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAyMAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIFAwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy+qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvid0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"t": 0.092117709,
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"server_name": "dns.google",
|
||||||
|
"alpn": [
|
||||||
|
"h2",
|
||||||
|
"http/1.1"
|
||||||
|
],
|
||||||
|
"no_tls_verify": false,
|
||||||
|
"oddity": "",
|
||||||
|
"proto": "tcp",
|
||||||
|
"started": 0.047288542
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All the data formats we're using here are, by the way,
|
||||||
|
compatible with the data formats specified at
|
||||||
|
https://github.com/ooni/spec/tree/master/data-formats.
|
||||||
|
|
||||||
|
### Suggested follow-up experiments
|
||||||
|
|
||||||
|
Try to run experiments in the following scenarios, and
|
||||||
|
check the output JSON to familiarize with what changes in
|
||||||
|
different error conditions.
|
||||||
|
|
||||||
|
1. measurement that causes timeout
|
||||||
|
|
||||||
|
2. measurement with wrong SNI
|
||||||
|
|
||||||
|
3. measurement with self-signed certificate
|
||||||
|
|
||||||
|
4. measurement with expired certificate
|
||||||
|
|
||||||
|
5. measurement with connection reset during handshake
|
||||||
|
|
||||||
|
6. measurement with timeout during handshake
|
||||||
|
|
||||||
|
Here are the commands I used for each proposed exercise:
|
||||||
|
|
||||||
|
1. go run -race ./internal/tutorial/measurex/chapter04 -address 8.8.4.4:1
|
||||||
|
|
||||||
|
2. go run -race ./internal/tutorial/measurex/chapter04 -sni example.org
|
||||||
|
|
||||||
|
3. go run -race ./internal/tutorial/measurex/chapter04 -address 104.154.89.105:443 -sni self-signed.badssl.com
|
||||||
|
|
||||||
|
4. go run -race ./internal/tutorial/measurex/chapter04 -address 104.154.89.105:443 -sni expire.badssl.com
|
||||||
|
|
||||||
|
To emulate the two last scenario, if you're on Linux, a
|
||||||
|
possibility is building Jafar with this command:
|
||||||
|
|
||||||
|
```
|
||||||
|
go build -v ./internal/cmd/jafar
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, for example, to provoke a connection reset you
|
||||||
|
can run in a terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo ./jafar -iptables-reset-keyword dns.google
|
||||||
|
```
|
||||||
|
|
||||||
|
and you can run this tutorial with `dns.google` as
|
||||||
|
the SNI in another terminal.
|
||||||
|
|
||||||
|
Likewise, you can obtain a timeout using the
|
||||||
|
`-iptables-drop-keyword` flag instead.
|
||||||
|
|
||||||
|
(Jafar runs forever and censors. You need to use
|
||||||
|
`^C` to terminate it from running.)
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have seen how to measure TLS handshakes. We have seen how
|
||||||
|
this flow produces different output on different error conditions.
|
||||||
|
|
249
internal/tutorial/measurex/chapter04/main.go
Normal file
249
internal/tutorial/measurex/chapter04/main.go
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter IV: TLS handshaking
|
||||||
|
//
|
||||||
|
// This chapter describes measuring TLS handshakes.
|
||||||
|
//
|
||||||
|
// Without further ado, let's describe our example `main.go` program
|
||||||
|
// and let's use it to better understand how to measure that.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter04/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The initial part of the program is pretty much the same as the one
|
||||||
|
// used in previous chapters, so I will not add further comments.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sni := flag.String("sni", "dns.google", "domain to resolver")
|
||||||
|
address := flag.String("address", "8.8.4.4:443", "remote endpoint address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Connecting and handshaking.
|
||||||
|
//
|
||||||
|
// We call the `ConnectAndHandshake` method. The arguments
|
||||||
|
// are the context, the address, and a TLS config.
|
||||||
|
//
|
||||||
|
// Under the hood, the code will call the TCP connect functionality
|
||||||
|
// we have seen in chapter02, using the address argument. Then, if
|
||||||
|
// successful, it will TLS handshake using the given TLS config.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m := mx.TLSConnectAndHandshake(ctx, *address, &tls.Config{
|
||||||
|
ServerName: *sni,
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
RootCAs: netxlite.NewDefaultCertPool(),
|
||||||
|
})
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Regarding the TLS config, in particular,
|
||||||
|
// the three fields above are the files you should always set
|
||||||
|
// in a TLS config when doing handshakes manually. The `ServerName`
|
||||||
|
// field forces the SNI, the NextProtos field forces the ALPN,
|
||||||
|
// and the `RootCAs` field is overridden so that we use the
|
||||||
|
// CA bundle that we bundle with OONI. (This CA bundle is the
|
||||||
|
// same you can find at https://curl.haxx.se/ca/.)
|
||||||
|
//
|
||||||
|
// As usual, the method to perform a measurement returns
|
||||||
|
// the measurement itself, which we print below.
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// As before, let us start off with a vanilla run:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter04
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Let us comment the JSON in detail:
|
||||||
|
//
|
||||||
|
// ```JavaScript
|
||||||
|
// {
|
||||||
|
// "network": "tcp",
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
//
|
||||||
|
// // This block is generated when connecting to a TCP
|
||||||
|
// // socket, as we've already seen in chapter02
|
||||||
|
// "connect": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "operation": "connect",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.046959084,
|
||||||
|
// "started": 0.022998875,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // These are the I/O events during the handshake
|
||||||
|
// "read_write": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 280,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.048752875,
|
||||||
|
// "started": 0.04874125,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 517,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.087221334,
|
||||||
|
// "started": 0.048760417,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 4301,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.088843584,
|
||||||
|
// "started": 0.088830959,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 64,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.092078042,
|
||||||
|
// "started": 0.092064042,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // This block contains information about the handshake
|
||||||
|
// "tls_handshake": [
|
||||||
|
// {
|
||||||
|
// "cipher_suite": "TLS_AES_128_GCM_SHA256",
|
||||||
|
// "failure": null,
|
||||||
|
// "negotiated_proto": "h2",
|
||||||
|
// "tls_version": "TLSv1.3",
|
||||||
|
// "peer_certificates": [
|
||||||
|
// {
|
||||||
|
// "data": "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",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "data": "MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAwMDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzpkgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsXlOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcmBA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKAgOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwLtmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYDVR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYGCCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcwAoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcGA1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3BraS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcNAQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQcSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrLRklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U+o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2YrPxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IERlQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGsYye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjOz23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJGAJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKwjuDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "data": "MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UECxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYxOTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwSiV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351kKSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZDrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zkj5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esWCruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35EiEua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbapsZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAfBgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUHMAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6AloCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAyMAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIFAwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy+qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvid0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "t": 0.092117709,
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "server_name": "dns.google",
|
||||||
|
// "alpn": [
|
||||||
|
// "h2",
|
||||||
|
// "http/1.1"
|
||||||
|
// ],
|
||||||
|
// "no_tls_verify": false,
|
||||||
|
// "oddity": "",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "started": 0.047288542
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// All the data formats we're using here are, by the way,
|
||||||
|
// compatible with the data formats specified at
|
||||||
|
// https://github.com/ooni/spec/tree/master/data-formats.
|
||||||
|
//
|
||||||
|
// ### Suggested follow-up experiments
|
||||||
|
//
|
||||||
|
// Try to run experiments in the following scenarios, and
|
||||||
|
// check the output JSON to familiarize with what changes in
|
||||||
|
// different error conditions.
|
||||||
|
//
|
||||||
|
// 1. measurement that causes timeout
|
||||||
|
//
|
||||||
|
// 2. measurement with wrong SNI
|
||||||
|
//
|
||||||
|
// 3. measurement with self-signed certificate
|
||||||
|
//
|
||||||
|
// 4. measurement with expired certificate
|
||||||
|
//
|
||||||
|
// 5. measurement with connection reset during handshake
|
||||||
|
//
|
||||||
|
// 6. measurement with timeout during handshake
|
||||||
|
//
|
||||||
|
// Here are the commands I used for each proposed exercise:
|
||||||
|
//
|
||||||
|
// 1. go run -race ./internal/tutorial/measurex/chapter04 -address 8.8.4.4:1
|
||||||
|
//
|
||||||
|
// 2. go run -race ./internal/tutorial/measurex/chapter04 -sni example.org
|
||||||
|
//
|
||||||
|
// 3. go run -race ./internal/tutorial/measurex/chapter04 -address 104.154.89.105:443 -sni self-signed.badssl.com
|
||||||
|
//
|
||||||
|
// 4. go run -race ./internal/tutorial/measurex/chapter04 -address 104.154.89.105:443 -sni expire.badssl.com
|
||||||
|
//
|
||||||
|
// To emulate the two last scenario, if you're on Linux, a
|
||||||
|
// possibility is building Jafar with this command:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go build -v ./internal/cmd/jafar
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Then, for example, to provoke a connection reset you
|
||||||
|
// can run in a terminal:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// sudo ./jafar -iptables-reset-keyword dns.google
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// and you can run this tutorial with `dns.google` as
|
||||||
|
// the SNI in another terminal.
|
||||||
|
//
|
||||||
|
// Likewise, you can obtain a timeout using the
|
||||||
|
// `-iptables-drop-keyword` flag instead.
|
||||||
|
//
|
||||||
|
// (Jafar runs forever and censors. You need to use
|
||||||
|
// `^C` to terminate it from running.)
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have seen how to measure TLS handshakes. We have seen how
|
||||||
|
// this flow produces different output on different error conditions.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
280
internal/tutorial/measurex/chapter05/README.md
Normal file
280
internal/tutorial/measurex/chapter05/README.md
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
|
||||||
|
# Chapter V: QUIC handshaking
|
||||||
|
|
||||||
|
This chapter describes measuring QUIC handshakes. Conceptually,
|
||||||
|
and code wise, this is very similar to the previous chapter.
|
||||||
|
The API call, in fact, has exactly the same structure, though
|
||||||
|
under the hood QUIC is different because there are no
|
||||||
|
separate connection establishment and handshake primitives.
|
||||||
|
For this reason, we will not see a connect event, but we
|
||||||
|
will only see a "QUIC handshake event".
|
||||||
|
|
||||||
|
Having said that, let us now move on and see the code of
|
||||||
|
the simple program that shows this functionality.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measure/chapter05/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The initial part of the program is pretty much the same as the one
|
||||||
|
used in previous chapters, so I will not add further comments.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sni := flag.String("sni", "dns.google", "value for SNI extension")
|
||||||
|
address := flag.String("address", "8.8.4.4:443", "remote endpoint address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handshaking with QUIC
|
||||||
|
|
||||||
|
The API signature is indeed the same as the previous chapter,
|
||||||
|
except that here we call the `QUICHandshake` function.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m := mx.QUICHandshake(ctx, *address, &tls.Config{
|
||||||
|
ServerName: *sni,
|
||||||
|
NextProtos: []string{"h3"},
|
||||||
|
RootCAs: netxlite.NewDefaultCertPool(),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The same remarks mentioned in the previous chapter regarding
|
||||||
|
the arguments for the TLS config also apply here. We need
|
||||||
|
to specify the SNI (`ServerName`), the ALPN (`NextProtos`),
|
||||||
|
and the CA pool we want to use. Here, again, we're using
|
||||||
|
the CA pool from cURL that we bundle with ooniprobe.
|
||||||
|
|
||||||
|
As we did in the previous chapters, here's the usual three
|
||||||
|
lines of code for printing the resulting measurement.
|
||||||
|
|
||||||
|
```
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
As before, let us start off with a vanilla run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter05
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces this JSON:
|
||||||
|
|
||||||
|
```JavaScript
|
||||||
|
{
|
||||||
|
// In chapter02 these two fields were similar but
|
||||||
|
// the network was "tcp" as opposed to "quic"
|
||||||
|
"network": "quic",
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
|
||||||
|
// This block contains I/O operations. Note that
|
||||||
|
// the protocol is "quic" and that the syscalls
|
||||||
|
// are "read_from" and "write_to" because QUIC does
|
||||||
|
// not bind/connect sockets. (The real syscalls
|
||||||
|
// are actually `recvfrom` and `sendto` but here
|
||||||
|
// we follow the Go convention of using read/write
|
||||||
|
// more frequently than send/recv.)
|
||||||
|
"read_write": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 1252,
|
||||||
|
"operation": "write_to",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.003903167,
|
||||||
|
"started": 0.0037395,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 1252,
|
||||||
|
"operation": "read_from",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.029389125,
|
||||||
|
"started": 0.002954792,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 1252,
|
||||||
|
"operation": "write_to",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.029757584,
|
||||||
|
"started": 0.02972325,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 1252,
|
||||||
|
"operation": "read_from",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.045039875,
|
||||||
|
"started": 0.029424792,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 1252,
|
||||||
|
"operation": "read_from",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.045055334,
|
||||||
|
"started": 0.045049625,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 1252,
|
||||||
|
"operation": "read_from",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.045073917,
|
||||||
|
"started": 0.045069667,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 1233,
|
||||||
|
"operation": "read_from",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.04508,
|
||||||
|
"started": 0.045075292,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 64,
|
||||||
|
"operation": "read_from",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.045088167,
|
||||||
|
"started": 0.045081167,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 44,
|
||||||
|
"operation": "write_to",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.045370417,
|
||||||
|
"started": 0.045338667,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 44,
|
||||||
|
"operation": "write_to",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.045392125,
|
||||||
|
"started": 0.045380959,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 83,
|
||||||
|
"operation": "write_to",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.047042542,
|
||||||
|
"started": 0.047001917,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 33,
|
||||||
|
"operation": "write_to",
|
||||||
|
"proto": "quic",
|
||||||
|
"t": 0.047060834,
|
||||||
|
"started": 0.047046875,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// This section describes the QUIC handshake and it has
|
||||||
|
// basically the same fields of the TLS handshake.
|
||||||
|
"quic_handshake": [
|
||||||
|
{
|
||||||
|
"cipher_suite": "TLS_CHACHA20_POLY1305_SHA256",
|
||||||
|
"failure": null,
|
||||||
|
"negotiated_proto": "h3",
|
||||||
|
"tls_version": "TLSv1.3",
|
||||||
|
"peer_certificates": [
|
||||||
|
{
|
||||||
|
"data": "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",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAwMDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzpkgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsXlOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcmBA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKAgOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwLtmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYDVR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYGCCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcwAoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcGA1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3BraS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcNAQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQcSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrLRklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U+o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2YrPxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IERlQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGsYye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjOz23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJGAJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKwjuDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UECxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYxOTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwSiV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351kKSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZDrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zkj5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esWCruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35EiEua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbapsZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAfBgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUHMAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6AloCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAyMAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIFAwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy+qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvid0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"t": 0.047042459,
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"server_name": "dns.google",
|
||||||
|
"alpn": [
|
||||||
|
"h3"
|
||||||
|
],
|
||||||
|
"no_tls_verify": false,
|
||||||
|
"oddity": "",
|
||||||
|
"proto": "quic",
|
||||||
|
"started": 0.002154834
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here are some suggestions on other experiments to run:
|
||||||
|
|
||||||
|
1. obtain a timeout by connecting on a port that is not
|
||||||
|
actually listening for QUIC;
|
||||||
|
|
||||||
|
2. obtain a certificate validation error by forcing
|
||||||
|
a different SNI;
|
||||||
|
|
||||||
|
3. use a different ALPN (by changing the code), and see
|
||||||
|
how the error and the oddity are handled. Can we do
|
||||||
|
anything about this by changing `./internal/netxlite/errorx`
|
||||||
|
to better support for this specific error condition?
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have seen how to perform QUIC handshake and
|
||||||
|
collect measurements.
|
||||||
|
|
282
internal/tutorial/measurex/chapter05/main.go
Normal file
282
internal/tutorial/measurex/chapter05/main.go
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter V: QUIC handshaking
|
||||||
|
//
|
||||||
|
// This chapter describes measuring QUIC handshakes. Conceptually,
|
||||||
|
// and code wise, this is very similar to the previous chapter.
|
||||||
|
// The API call, in fact, has exactly the same structure, though
|
||||||
|
// under the hood QUIC is different because there are no
|
||||||
|
// separate connection establishment and handshake primitives.
|
||||||
|
// For this reason, we will not see a connect event, but we
|
||||||
|
// will only see a "QUIC handshake event".
|
||||||
|
//
|
||||||
|
// Having said that, let us now move on and see the code of
|
||||||
|
// the simple program that shows this functionality.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measure/chapter05/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The initial part of the program is pretty much the same as the one
|
||||||
|
// used in previous chapters, so I will not add further comments.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sni := flag.String("sni", "dns.google", "value for SNI extension")
|
||||||
|
address := flag.String("address", "8.8.4.4:443", "remote endpoint address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Handshaking with QUIC
|
||||||
|
//
|
||||||
|
// The API signature is indeed the same as the previous chapter,
|
||||||
|
// except that here we call the `QUICHandshake` function.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m := mx.QUICHandshake(ctx, *address, &tls.Config{
|
||||||
|
ServerName: *sni,
|
||||||
|
NextProtos: []string{"h3"},
|
||||||
|
RootCAs: netxlite.NewDefaultCertPool(),
|
||||||
|
})
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The same remarks mentioned in the previous chapter regarding
|
||||||
|
// the arguments for the TLS config also apply here. We need
|
||||||
|
// to specify the SNI (`ServerName`), the ALPN (`NextProtos`),
|
||||||
|
// and the CA pool we want to use. Here, again, we're using
|
||||||
|
// the CA pool from cURL that we bundle with ooniprobe.
|
||||||
|
//
|
||||||
|
// As we did in the previous chapters, here's the usual three
|
||||||
|
// lines of code for printing the resulting measurement.
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// As before, let us start off with a vanilla run:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter05
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Produces this JSON:
|
||||||
|
//
|
||||||
|
// ```JavaScript
|
||||||
|
// {
|
||||||
|
// // In chapter02 these two fields were similar but
|
||||||
|
// // the network was "tcp" as opposed to "quic"
|
||||||
|
// "network": "quic",
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
//
|
||||||
|
// // This block contains I/O operations. Note that
|
||||||
|
// // the protocol is "quic" and that the syscalls
|
||||||
|
// // are "read_from" and "write_to" because QUIC does
|
||||||
|
// // not bind/connect sockets. (The real syscalls
|
||||||
|
// // are actually `recvfrom` and `sendto` but here
|
||||||
|
// // we follow the Go convention of using read/write
|
||||||
|
// // more frequently than send/recv.)
|
||||||
|
// "read_write": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 1252,
|
||||||
|
// "operation": "write_to",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.003903167,
|
||||||
|
// "started": 0.0037395,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 1252,
|
||||||
|
// "operation": "read_from",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.029389125,
|
||||||
|
// "started": 0.002954792,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 1252,
|
||||||
|
// "operation": "write_to",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.029757584,
|
||||||
|
// "started": 0.02972325,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 1252,
|
||||||
|
// "operation": "read_from",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.045039875,
|
||||||
|
// "started": 0.029424792,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 1252,
|
||||||
|
// "operation": "read_from",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.045055334,
|
||||||
|
// "started": 0.045049625,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 1252,
|
||||||
|
// "operation": "read_from",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.045073917,
|
||||||
|
// "started": 0.045069667,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 1233,
|
||||||
|
// "operation": "read_from",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.04508,
|
||||||
|
// "started": 0.045075292,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 64,
|
||||||
|
// "operation": "read_from",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.045088167,
|
||||||
|
// "started": 0.045081167,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 44,
|
||||||
|
// "operation": "write_to",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.045370417,
|
||||||
|
// "started": 0.045338667,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 44,
|
||||||
|
// "operation": "write_to",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.045392125,
|
||||||
|
// "started": 0.045380959,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 83,
|
||||||
|
// "operation": "write_to",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.047042542,
|
||||||
|
// "started": 0.047001917,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 33,
|
||||||
|
// "operation": "write_to",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "t": 0.047060834,
|
||||||
|
// "started": 0.047046875,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // This section describes the QUIC handshake and it has
|
||||||
|
// // basically the same fields of the TLS handshake.
|
||||||
|
// "quic_handshake": [
|
||||||
|
// {
|
||||||
|
// "cipher_suite": "TLS_CHACHA20_POLY1305_SHA256",
|
||||||
|
// "failure": null,
|
||||||
|
// "negotiated_proto": "h3",
|
||||||
|
// "tls_version": "TLSv1.3",
|
||||||
|
// "peer_certificates": [
|
||||||
|
// {
|
||||||
|
// "data": "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",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "data": "MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAwMDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzpkgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsXlOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcmBA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKAgOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwLtmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYDVR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYGCCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcwAoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcGA1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3BraS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcNAQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQcSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrLRklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U+o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2YrPxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IERlQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGsYye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjOz23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJGAJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKwjuDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "data": "MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UECxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYxOTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwSiV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351kKSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZDrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zkj5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esWCruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35EiEua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbapsZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAfBgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUHMAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6AloCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAyMAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIFAwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy+qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvid0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "t": 0.047042459,
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "server_name": "dns.google",
|
||||||
|
// "alpn": [
|
||||||
|
// "h3"
|
||||||
|
// ],
|
||||||
|
// "no_tls_verify": false,
|
||||||
|
// "oddity": "",
|
||||||
|
// "proto": "quic",
|
||||||
|
// "started": 0.002154834
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Here are some suggestions on other experiments to run:
|
||||||
|
//
|
||||||
|
// 1. obtain a timeout by connecting on a port that is not
|
||||||
|
// actually listening for QUIC;
|
||||||
|
//
|
||||||
|
// 2. obtain a certificate validation error by forcing
|
||||||
|
// a different SNI;
|
||||||
|
//
|
||||||
|
// 3. use a different ALPN (by changing the code), and see
|
||||||
|
// how the error and the oddity are handled. Can we do
|
||||||
|
// anything about this by changing `./internal/netxlite/errorx`
|
||||||
|
// to better support for this specific error condition?
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have seen how to perform QUIC handshake and
|
||||||
|
// collect measurements.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
486
internal/tutorial/measurex/chapter06/README.md
Normal file
486
internal/tutorial/measurex/chapter06/README.md
Normal file
|
@ -0,0 +1,486 @@
|
||||||
|
|
||||||
|
# Chapter VI: Getting a webpage from an HTTP/HTTPS/HTTP3 endpoint.
|
||||||
|
|
||||||
|
This chapter describes measuring getting a webpage from an
|
||||||
|
HTTPS endpoint. We have seen how to TCP connect, we have
|
||||||
|
seen how to TLS handshake, now it's time to see how we can
|
||||||
|
combine these operations with fetching a webpage from a
|
||||||
|
given TCP endpoint speaking HTTP and TLS. (As well as to
|
||||||
|
provide you with information on how to otherwise fetch
|
||||||
|
from HTTP and HTTP/3 endpoints.)
|
||||||
|
|
||||||
|
The program we're going to write, `main.go`, will show a
|
||||||
|
high-level operation to perform this measurement in a
|
||||||
|
single API call. The code implementing this API call will
|
||||||
|
combine the operations we have seen in previous chapter
|
||||||
|
with the "give me the webpage" operation. We are still
|
||||||
|
quite far away from the ability of "measuring a URL" but
|
||||||
|
we are increasingly moving towards more complex operations.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter06/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
We have package declaration and imports as usual.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
We have factored the three lines to print a measurement
|
||||||
|
into the following utility function called `print`.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The initial part of the program is pretty much the same as the one
|
||||||
|
used in previous chapters, expect that we have a few more command line
|
||||||
|
flags now, so I will not add further comments.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
func main() {
|
||||||
|
sni := flag.String("sni", "dns.google", "value for SNI extension")
|
||||||
|
address := flag.String("address", "8.8.4.4:443", "remote endpoint address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTPEndpoint: a description of what to measure
|
||||||
|
|
||||||
|
First of all, let us create a description of the endpoint
|
||||||
|
for `measurex`. Up to this point we have used endpoint
|
||||||
|
to describe a level-4-like address. Therefore, we have seen
|
||||||
|
TCP endpoints being used for the TCP connect and the TLS
|
||||||
|
handshake, and we have seen QUIC endpoints being used
|
||||||
|
for the QUIC handshake. Now, however, we are going
|
||||||
|
to need more information to characterize the endpoint.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
epnt := &measurex.HTTPEndpoint{
|
||||||
|
Domain: *sni,
|
||||||
|
Network: "tcp",
|
||||||
|
Address: *address,
|
||||||
|
SNI: *sni,
|
||||||
|
ALPN: []string{"h2", "http/1.1"},
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: *sni,
|
||||||
|
Path: "/",
|
||||||
|
},
|
||||||
|
Header: measurex.NewHTTPRequestHeaderForMeasuring(),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In fact, in the above definition we recognize fields
|
||||||
|
we have already discussed, such as:
|
||||||
|
|
||||||
|
- `Network`, describing whether to use "tcp" or "quic";
|
||||||
|
|
||||||
|
- `Address`, which is the endpoint address.
|
||||||
|
|
||||||
|
But we also need to combine into this view of the
|
||||||
|
endpoint additional fields for TLS:
|
||||||
|
|
||||||
|
- `SNI`, to set the SNI;
|
||||||
|
|
||||||
|
- `ALPN`, to set the ALPN;
|
||||||
|
|
||||||
|
But then we also need to specify:
|
||||||
|
|
||||||
|
- the URL to use;
|
||||||
|
|
||||||
|
- the headers to use (for which we're using a handy
|
||||||
|
factory for creating reasonable defaults for measuring).
|
||||||
|
|
||||||
|
This API is not the highest level API with which to do
|
||||||
|
the job, but it's still handy to introduce the
|
||||||
|
`measurex.HTTPEndpoint` data structure since it's
|
||||||
|
used by higher level APIs.
|
||||||
|
|
||||||
|
(You may also be wondering about the CA pool. It turns
|
||||||
|
out that for APIs such as this one and for higher
|
||||||
|
level APIs, the default is to always use the bundled
|
||||||
|
Mozilla CA pool, because this is what we use in
|
||||||
|
most cases for performing measurements.)
|
||||||
|
|
||||||
|
### HTTPEndpointGetWithoutCookies
|
||||||
|
|
||||||
|
When used with an HTTP URL, the `HTTPEndpointGetWithoutCookies`
|
||||||
|
method combines two operations:
|
||||||
|
|
||||||
|
- TCP connect
|
||||||
|
|
||||||
|
- HTTP GET
|
||||||
|
|
||||||
|
When the URL is HTTPS, we do:
|
||||||
|
|
||||||
|
- TCP connect
|
||||||
|
|
||||||
|
- TLS handshake
|
||||||
|
|
||||||
|
- HTTP GET (or HTTP/2 GET depending on the ALPN)
|
||||||
|
|
||||||
|
When the `HTTPEndpoint.Network` field value
|
||||||
|
is QUIC, instead we do:
|
||||||
|
|
||||||
|
- QUIC handshake
|
||||||
|
|
||||||
|
- HTTP/3 GET
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m := mx.HTTPEndpointGetWithoutCookies(ctx, epnt)
|
||||||
|
```
|
||||||
|
|
||||||
|
The arguments for `HTTPEndpointGetWithDBWithoutCookies` are:
|
||||||
|
|
||||||
|
- the context for deadline/timeout
|
||||||
|
|
||||||
|
- the HTTPEndpoint descriptor
|
||||||
|
|
||||||
|
The result is an `HTTPEndpointMeasurement` which
|
||||||
|
you can inspect with
|
||||||
|
|
||||||
|
```
|
||||||
|
go doc ./internal/measurex.HTTPEndpointMeasurement
|
||||||
|
```
|
||||||
|
|
||||||
|
### Printing the measurement
|
||||||
|
|
||||||
|
Let us now print the resulting measurement.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us perform a vanilla run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter06
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the JSON output. Let us comment it in detail:
|
||||||
|
|
||||||
|
```Javascript
|
||||||
|
{
|
||||||
|
// The returned type is called HTTPEndpointMeasurement
|
||||||
|
// and you see that here on top we indeed have the
|
||||||
|
// information on the endpoint and the URL.
|
||||||
|
"url": "https://dns.google/",
|
||||||
|
"network": "tcp",
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
|
||||||
|
// Internally, HTTPEndpointGetWithoutCookies calls
|
||||||
|
// TCPConnect and here we see the corresponding event
|
||||||
|
"connect": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"operation": "connect",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.02422375,
|
||||||
|
"started": 0.002269291,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// These are the I/O operations we have already seen
|
||||||
|
// in previous chapters
|
||||||
|
"read_write": [
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 280,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.024931791,
|
||||||
|
"started": 0.024910416,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 517,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.063629791,
|
||||||
|
"started": 0.024935666,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 4301,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.064183,
|
||||||
|
"started": 0.064144208,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 64,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.065464041,
|
||||||
|
"started": 0.065441333,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 86,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.067256083,
|
||||||
|
"started": 0.067224375,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 201,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.067674416,
|
||||||
|
"started": 0.067652375,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 93,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.086618708,
|
||||||
|
"started": 0.067599208,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 31,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.086703625,
|
||||||
|
"started": 0.0866745,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 2028,
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.337785916,
|
||||||
|
"started": 0.086717333,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 39,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.338514916,
|
||||||
|
"started": 0.338485375,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": null,
|
||||||
|
"num_bytes": 24,
|
||||||
|
"operation": "write",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.338800833,
|
||||||
|
"started": 0.338788625,
|
||||||
|
"oddity": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"failure": "connection_already_closed",
|
||||||
|
"operation": "read",
|
||||||
|
"proto": "tcp",
|
||||||
|
"t": 0.338888041,
|
||||||
|
"started": 0.338523291,
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Internally, HTTPEndpointGetWithoutCookies calls TLSConnectAndHandshake,
|
||||||
|
// and here's the resulting handshake event. Of course, if we
|
||||||
|
// specified a QUIC endpoint we would instead see here a
|
||||||
|
// QUIC handshake event. And, we would not see any handshake
|
||||||
|
// if the URL was instead an HTTP URL.
|
||||||
|
"tls_handshake": [
|
||||||
|
{
|
||||||
|
"cipher_suite": "TLS_AES_128_GCM_SHA256",
|
||||||
|
"failure": null,
|
||||||
|
"negotiated_proto": "h2",
|
||||||
|
"tls_version": "TLSv1.3",
|
||||||
|
"peer_certificates": [
|
||||||
|
{
|
||||||
|
"data": "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",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAwMDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzpkgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsXlOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcmBA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKAgOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwLtmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYDVR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYGCCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcwAoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcGA1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3BraS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcNAQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQcSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrLRklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U+o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2YrPxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IERlQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGsYye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjOz23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJGAJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKwjuDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UECxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYxOTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwSiV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351kKSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZDrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zkj5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esWCruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35EiEua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbapsZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAfBgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUHMAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6AloCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAyMAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIFAwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy+qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvid0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=",
|
||||||
|
"format": "base64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"t": 0.065514708,
|
||||||
|
"address": "8.8.4.4:443",
|
||||||
|
"server_name": "dns.google",
|
||||||
|
"alpn": [
|
||||||
|
"h2",
|
||||||
|
"http/1.1"
|
||||||
|
],
|
||||||
|
"no_tls_verify": false,
|
||||||
|
"oddity": "",
|
||||||
|
"proto": "tcp",
|
||||||
|
"started": 0.024404083
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Finally here we see information about the round trip, which
|
||||||
|
// is formatted according the df-001-httpt data format:
|
||||||
|
"http_round_trip": [
|
||||||
|
{
|
||||||
|
|
||||||
|
// This field indicates whether there was an error during
|
||||||
|
// the HTTP round trip:
|
||||||
|
"failure": null,
|
||||||
|
|
||||||
|
// This field contains the request method, URL, and HTTP headers
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "https://dns.google/",
|
||||||
|
"headers": {
|
||||||
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"accept-language": "en-US;q=0.8,en;q=0.5",
|
||||||
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// This field contains the response status code, body,
|
||||||
|
// and headers.
|
||||||
|
"response": {
|
||||||
|
"code": 200,
|
||||||
|
"headers": {
|
||||||
|
"accept-ranges": "none",
|
||||||
|
"alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
|
||||||
|
"cache-control": "private",
|
||||||
|
"content-security-policy": "object-src 'none';base-uri 'self';script-src 'nonce-bSLcJjaotppZl3Y2moIaxg==' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/honest_dns/1_0;frame-ancestors 'none'",
|
||||||
|
"content-type": "text/html; charset=UTF-8",
|
||||||
|
"date": "Fri, 24 Sep 2021 08:51:01 GMT",
|
||||||
|
"server": "scaffolding on HTTPServer2",
|
||||||
|
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
|
||||||
|
"vary": "Accept-Encoding",
|
||||||
|
"x-content-type-options": "nosniff",
|
||||||
|
"x-frame-options": "SAMEORIGIN",
|
||||||
|
"x-xss-protection": "0"
|
||||||
|
},
|
||||||
|
|
||||||
|
// The body in particular is a snapshot of the response
|
||||||
|
// body: we don't want to read and submit to the OONI
|
||||||
|
// collector large bodies.
|
||||||
|
"body": {
|
||||||
|
"data": "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4gPGhlYWQ+IDx0aXRsZT5Hb29nbGUgUHVibGljIEROUzwvdGl0bGU+ICA8bWV0YSBjaGFyc2V0PSJVVEYtOCI+IDxsaW5rIGhyZWY9Ii9zdGF0aWMvOTNkZDU5NTQvZmF2aWNvbi5wbmciIHJlbD0ic2hvcnRjdXQgaWNvbiIgdHlwZT0iaW1hZ2UvcG5nIj4gPGxpbmsgaHJlZj0iL3N0YXRpYy84MzZhZWJjNi9tYXR0ZXIubWluLmNzcyIgcmVsPSJzdHlsZXNoZWV0Ij4gPGxpbmsgaHJlZj0iL3N0YXRpYy9iODUzNmMzNy9zaGFyZWQuY3NzIiByZWw9InN0eWxlc2hlZXQiPiA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiPiAgPGxpbmsgaHJlZj0iL3N0YXRpYy9kMDVjZDZiYS9yb290LmNzcyIgcmVsPSJzdHlsZXNoZWV0Ij4gPC9oZWFkPiA8Ym9keT4gPHNwYW4gY2xhc3M9ImZpbGxlciB0b3AiPjwvc3Bhbj4gICA8ZGl2IGNsYXNzPSJsb2dvIiB0aXRsZT0iR29vZ2xlIFB1YmxpYyBETlMiPiA8ZGl2IGNsYXNzPSJsb2dvLXRleHQiPjxzcGFuPlB1YmxpYyBETlM8L3NwYW4+PC9kaXY+IDwvZGl2PiAgPGZvcm0gYWN0aW9uPSIvcXVlcnkiIG1ldGhvZD0iR0VUIj4gIDxkaXYgY2xhc3M9InJvdyI+IDxsYWJlbCBjbGFzcz0ibWF0dGVyLXRleHRmaWVsZC1vdXRsaW5lZCI+IDxpbnB1dCB0eXBlPSJ0ZXh0IiBuYW1lPSJuYW1lIiBwbGFjZWhvbGRlcj0iJm5ic3A7Ij4gPHNwYW4+RE5TIE5hbWU8L3NwYW4+IDxwIGNsYXNzPSJoZWxwIj4gRW50ZXIgYSBkb21haW4gKGxpa2UgZXhhbXBsZS5jb20pIG9yIElQIGFkZHJlc3MgKGxpa2UgOC44LjguOCBvciAyMDAxOjQ4NjA6NDg2MDo6ODg0NCkgaGVyZS4gPC9wPiA8L2xhYmVsPiA8YnV0dG9uIGNsYXNzPSJtYXR0ZXItYnV0dG9uLWNvbnRhaW5lZCBtYXR0ZXItcHJpbWFyeSIgdHlwZT0ic3VibWl0Ij5SZXNvbHZlPC9idXR0b24+IDwvZGl2PiA8L2Zvcm0+ICA8c3BhbiBjbGFzcz0iZmlsbGVyIGJvdHRvbSI+PC9zcGFuPiA8Zm9vdGVyIGNsYXNzPSJyb3ciPiA8YSBocmVmPSJodHRwczovL2RldmVsb3BlcnMuZ29vZ2xlLmNvbS9zcGVlZC9wdWJsaWMtZG5zIj5IZWxwPC9hPiA8YSBocmVmPSIvY2FjaGUiPkNhY2hlIEZsdXNoPC9hPiA8c3BhbiBjbGFzcz0iZmlsbGVyIj48L3NwYW4+IDxhIGhyZWY9Imh0dHBzOi8vZGV2ZWxvcGVycy5nb29nbGUuY29tL3NwZWVkL3B1YmxpYy1kbnMvZG9jcy91c2luZyI+IEdldCBTdGFydGVkIHdpdGggR29vZ2xlIFB1YmxpYyBETlMgPC9hPiA8L2Zvb3Rlcj4gICA8c2NyaXB0IG5vbmNlPSJiU0xjSmphb3RwcFpsM1kybW9JYXhnPT0iPmRvY3VtZW50LmZvcm1zWzBdLm5hbWUuZm9jdXMoKTs8L3NjcmlwdD4gPC9ib2R5PiA8L2h0bWw+",
|
||||||
|
"format": "base64"
|
||||||
|
},
|
||||||
|
|
||||||
|
// This field tells us whether the size of the read
|
||||||
|
// snapshot was smaller than the snapshot size. If
|
||||||
|
// not, then the body has been truncated.
|
||||||
|
"body_is_truncated": false,
|
||||||
|
|
||||||
|
// These extra fields are not part of the spec and
|
||||||
|
// hence we prefix them with `x_`. They tell us
|
||||||
|
// the length of the body and whether the content
|
||||||
|
// of the body is valid UTF8.
|
||||||
|
"x_body_length": 1383,
|
||||||
|
"x_body_is_utf8": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// The t field is the moment where we finished the
|
||||||
|
// round trip and saved the event. The started field
|
||||||
|
// is instead when we started the round trip.
|
||||||
|
|
||||||
|
// You may notice that the start of the round trip
|
||||||
|
// if after the `t` of the handshake. This tells us
|
||||||
|
// that the code first connects, then handshakes, and
|
||||||
|
// finally creates HTTP code for performing the
|
||||||
|
// round trip.
|
||||||
|
"t": 0.338674625,
|
||||||
|
"started": 0.065926625,
|
||||||
|
|
||||||
|
// As usual we also compute an oddity value related
|
||||||
|
// in this case to the HTTP round trip.
|
||||||
|
"oddity": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here are some suggestions for follow up measurements:
|
||||||
|
|
||||||
|
1. provoke a connect error by using:
|
||||||
|
|
||||||
|
```
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter06 -address 127.0.0.1:1
|
||||||
|
```
|
||||||
|
|
||||||
|
2. provoke a TLS handshake error by using:
|
||||||
|
|
||||||
|
```
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter06 -sni example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
3. provoke an HTTP round trip error by using:
|
||||||
|
|
||||||
|
```
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter06 -address 8.8.8.8:853
|
||||||
|
```
|
||||||
|
|
||||||
|
4. modify the code to fetch an HTTP endpoint instead (hint: you
|
||||||
|
need to change the HTTPEndpoint's URL scheme);
|
||||||
|
|
||||||
|
5. modify the code to use QUIC and HTTP/3 instead (hint: you need to
|
||||||
|
change the HTTPEndpoint's network and... is this enough?).
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have seen how to measure the flow of fetching a
|
||||||
|
specific webpage from an HTTPEndpoint.
|
||||||
|
|
488
internal/tutorial/measurex/chapter06/main.go
Normal file
488
internal/tutorial/measurex/chapter06/main.go
Normal file
|
@ -0,0 +1,488 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter VI: Getting a webpage from an HTTP/HTTPS/HTTP3 endpoint.
|
||||||
|
//
|
||||||
|
// This chapter describes measuring getting a webpage from an
|
||||||
|
// HTTPS endpoint. We have seen how to TCP connect, we have
|
||||||
|
// seen how to TLS handshake, now it's time to see how we can
|
||||||
|
// combine these operations with fetching a webpage from a
|
||||||
|
// given TCP endpoint speaking HTTP and TLS. (As well as to
|
||||||
|
// provide you with information on how to otherwise fetch
|
||||||
|
// from HTTP and HTTP/3 endpoints.)
|
||||||
|
//
|
||||||
|
// The program we're going to write, `main.go`, will show a
|
||||||
|
// high-level operation to perform this measurement in a
|
||||||
|
// single API call. The code implementing this API call will
|
||||||
|
// combine the operations we have seen in previous chapter
|
||||||
|
// with the "give me the webpage" operation. We are still
|
||||||
|
// quite far away from the ability of "measuring a URL" but
|
||||||
|
// we are increasingly moving towards more complex operations.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter06/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// We have package declaration and imports as usual.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We have factored the three lines to print a measurement
|
||||||
|
// into the following utility function called `print`.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The initial part of the program is pretty much the same as the one
|
||||||
|
// used in previous chapters, expect that we have a few more command line
|
||||||
|
// flags now, so I will not add further comments.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
func main() {
|
||||||
|
sni := flag.String("sni", "dns.google", "value for SNI extension")
|
||||||
|
address := flag.String("address", "8.8.4.4:443", "remote endpoint address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### HTTPEndpoint: a description of what to measure
|
||||||
|
//
|
||||||
|
// First of all, let us create a description of the endpoint
|
||||||
|
// for `measurex`. Up to this point we have used endpoint
|
||||||
|
// to describe a level-4-like address. Therefore, we have seen
|
||||||
|
// TCP endpoints being used for the TCP connect and the TLS
|
||||||
|
// handshake, and we have seen QUIC endpoints being used
|
||||||
|
// for the QUIC handshake. Now, however, we are going
|
||||||
|
// to need more information to characterize the endpoint.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
epnt := &measurex.HTTPEndpoint{
|
||||||
|
Domain: *sni,
|
||||||
|
Network: "tcp",
|
||||||
|
Address: *address,
|
||||||
|
SNI: *sni,
|
||||||
|
ALPN: []string{"h2", "http/1.1"},
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: *sni,
|
||||||
|
Path: "/",
|
||||||
|
},
|
||||||
|
Header: measurex.NewHTTPRequestHeaderForMeasuring(),
|
||||||
|
}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// In fact, in the above definition we recognize fields
|
||||||
|
// we have already discussed, such as:
|
||||||
|
//
|
||||||
|
// - `Network`, describing whether to use "tcp" or "quic";
|
||||||
|
//
|
||||||
|
// - `Address`, which is the endpoint address.
|
||||||
|
//
|
||||||
|
// But we also need to combine into this view of the
|
||||||
|
// endpoint additional fields for TLS:
|
||||||
|
//
|
||||||
|
// - `SNI`, to set the SNI;
|
||||||
|
//
|
||||||
|
// - `ALPN`, to set the ALPN;
|
||||||
|
//
|
||||||
|
// But then we also need to specify:
|
||||||
|
//
|
||||||
|
// - the URL to use;
|
||||||
|
//
|
||||||
|
// - the headers to use (for which we're using a handy
|
||||||
|
// factory for creating reasonable defaults for measuring).
|
||||||
|
//
|
||||||
|
// This API is not the highest level API with which to do
|
||||||
|
// the job, but it's still handy to introduce the
|
||||||
|
// `measurex.HTTPEndpoint` data structure since it's
|
||||||
|
// used by higher level APIs.
|
||||||
|
//
|
||||||
|
// (You may also be wondering about the CA pool. It turns
|
||||||
|
// out that for APIs such as this one and for higher
|
||||||
|
// level APIs, the default is to always use the bundled
|
||||||
|
// Mozilla CA pool, because this is what we use in
|
||||||
|
// most cases for performing measurements.)
|
||||||
|
//
|
||||||
|
// ### HTTPEndpointGetWithoutCookies
|
||||||
|
//
|
||||||
|
// When used with an HTTP URL, the `HTTPEndpointGetWithoutCookies`
|
||||||
|
// method combines two operations:
|
||||||
|
//
|
||||||
|
// - TCP connect
|
||||||
|
//
|
||||||
|
// - HTTP GET
|
||||||
|
//
|
||||||
|
// When the URL is HTTPS, we do:
|
||||||
|
//
|
||||||
|
// - TCP connect
|
||||||
|
//
|
||||||
|
// - TLS handshake
|
||||||
|
//
|
||||||
|
// - HTTP GET (or HTTP/2 GET depending on the ALPN)
|
||||||
|
//
|
||||||
|
// When the `HTTPEndpoint.Network` field value
|
||||||
|
// is QUIC, instead we do:
|
||||||
|
//
|
||||||
|
// - QUIC handshake
|
||||||
|
//
|
||||||
|
// - HTTP/3 GET
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m := mx.HTTPEndpointGetWithoutCookies(ctx, epnt)
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The arguments for `HTTPEndpointGetWithDBWithoutCookies` are:
|
||||||
|
//
|
||||||
|
// - the context for deadline/timeout
|
||||||
|
//
|
||||||
|
// - the HTTPEndpoint descriptor
|
||||||
|
//
|
||||||
|
// The result is an `HTTPEndpointMeasurement` which
|
||||||
|
// you can inspect with
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go doc ./internal/measurex.HTTPEndpointMeasurement
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Printing the measurement
|
||||||
|
//
|
||||||
|
// Let us now print the resulting measurement.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us perform a vanilla run first:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter06
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is the JSON output. Let us comment it in detail:
|
||||||
|
//
|
||||||
|
// ```Javascript
|
||||||
|
// {
|
||||||
|
// // The returned type is called HTTPEndpointMeasurement
|
||||||
|
// // and you see that here on top we indeed have the
|
||||||
|
// // information on the endpoint and the URL.
|
||||||
|
// "url": "https://dns.google/",
|
||||||
|
// "network": "tcp",
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
//
|
||||||
|
// // Internally, HTTPEndpointGetWithoutCookies calls
|
||||||
|
// // TCPConnect and here we see the corresponding event
|
||||||
|
// "connect": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "operation": "connect",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.02422375,
|
||||||
|
// "started": 0.002269291,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // These are the I/O operations we have already seen
|
||||||
|
// // in previous chapters
|
||||||
|
// "read_write": [
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 280,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.024931791,
|
||||||
|
// "started": 0.024910416,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 517,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.063629791,
|
||||||
|
// "started": 0.024935666,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 4301,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.064183,
|
||||||
|
// "started": 0.064144208,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 64,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.065464041,
|
||||||
|
// "started": 0.065441333,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 86,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.067256083,
|
||||||
|
// "started": 0.067224375,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 201,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.067674416,
|
||||||
|
// "started": 0.067652375,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 93,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.086618708,
|
||||||
|
// "started": 0.067599208,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 31,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.086703625,
|
||||||
|
// "started": 0.0866745,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 2028,
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.337785916,
|
||||||
|
// "started": 0.086717333,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 39,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.338514916,
|
||||||
|
// "started": 0.338485375,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": null,
|
||||||
|
// "num_bytes": 24,
|
||||||
|
// "operation": "write",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.338800833,
|
||||||
|
// "started": 0.338788625,
|
||||||
|
// "oddity": ""
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "failure": "connection_already_closed",
|
||||||
|
// "operation": "read",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "t": 0.338888041,
|
||||||
|
// "started": 0.338523291,
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // Internally, HTTPEndpointGetWithoutCookies calls TLSConnectAndHandshake,
|
||||||
|
// // and here's the resulting handshake event. Of course, if we
|
||||||
|
// // specified a QUIC endpoint we would instead see here a
|
||||||
|
// // QUIC handshake event. And, we would not see any handshake
|
||||||
|
// // if the URL was instead an HTTP URL.
|
||||||
|
// "tls_handshake": [
|
||||||
|
// {
|
||||||
|
// "cipher_suite": "TLS_AES_128_GCM_SHA256",
|
||||||
|
// "failure": null,
|
||||||
|
// "negotiated_proto": "h2",
|
||||||
|
// "tls_version": "TLSv1.3",
|
||||||
|
// "peer_certificates": [
|
||||||
|
// {
|
||||||
|
// "data": "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",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "data": "MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAwMDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzpkgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsXlOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcmBA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKAgOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwLtmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYDVR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYGCCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcwAoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcGA1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3BraS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcNAQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQcSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrLRklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U+o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2YrPxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IERlQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGsYye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjOz23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJGAJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKwjuDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "data": "MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UECxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYxOTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwSiV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351kKSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZDrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zkj5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esWCruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35EiEua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbapsZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAfBgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUHMAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6AloCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAyMAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIFAwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy+qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvid0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=",
|
||||||
|
// "format": "base64"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "t": 0.065514708,
|
||||||
|
// "address": "8.8.4.4:443",
|
||||||
|
// "server_name": "dns.google",
|
||||||
|
// "alpn": [
|
||||||
|
// "h2",
|
||||||
|
// "http/1.1"
|
||||||
|
// ],
|
||||||
|
// "no_tls_verify": false,
|
||||||
|
// "oddity": "",
|
||||||
|
// "proto": "tcp",
|
||||||
|
// "started": 0.024404083
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// // Finally here we see information about the round trip, which
|
||||||
|
// // is formatted according the df-001-httpt data format:
|
||||||
|
// "http_round_trip": [
|
||||||
|
// {
|
||||||
|
//
|
||||||
|
// // This field indicates whether there was an error during
|
||||||
|
// // the HTTP round trip:
|
||||||
|
// "failure": null,
|
||||||
|
//
|
||||||
|
// // This field contains the request method, URL, and HTTP headers
|
||||||
|
// "request": {
|
||||||
|
// "method": "GET",
|
||||||
|
// "url": "https://dns.google/",
|
||||||
|
// "headers": {
|
||||||
|
// "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
// "accept-language": "en-US;q=0.8,en;q=0.5",
|
||||||
|
// "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// // This field contains the response status code, body,
|
||||||
|
// // and headers.
|
||||||
|
// "response": {
|
||||||
|
// "code": 200,
|
||||||
|
// "headers": {
|
||||||
|
// "accept-ranges": "none",
|
||||||
|
// "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
|
||||||
|
// "cache-control": "private",
|
||||||
|
// "content-security-policy": "object-src 'none';base-uri 'self';script-src 'nonce-bSLcJjaotppZl3Y2moIaxg==' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/honest_dns/1_0;frame-ancestors 'none'",
|
||||||
|
// "content-type": "text/html; charset=UTF-8",
|
||||||
|
// "date": "Fri, 24 Sep 2021 08:51:01 GMT",
|
||||||
|
// "server": "scaffolding on HTTPServer2",
|
||||||
|
// "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
|
||||||
|
// "vary": "Accept-Encoding",
|
||||||
|
// "x-content-type-options": "nosniff",
|
||||||
|
// "x-frame-options": "SAMEORIGIN",
|
||||||
|
// "x-xss-protection": "0"
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// // The body in particular is a snapshot of the response
|
||||||
|
// // body: we don't want to read and submit to the OONI
|
||||||
|
// // collector large bodies.
|
||||||
|
// "body": {
|
||||||
|
// "data": "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4gPGhlYWQ+IDx0aXRsZT5Hb29nbGUgUHVibGljIEROUzwvdGl0bGU+ICA8bWV0YSBjaGFyc2V0PSJVVEYtOCI+IDxsaW5rIGhyZWY9Ii9zdGF0aWMvOTNkZDU5NTQvZmF2aWNvbi5wbmciIHJlbD0ic2hvcnRjdXQgaWNvbiIgdHlwZT0iaW1hZ2UvcG5nIj4gPGxpbmsgaHJlZj0iL3N0YXRpYy84MzZhZWJjNi9tYXR0ZXIubWluLmNzcyIgcmVsPSJzdHlsZXNoZWV0Ij4gPGxpbmsgaHJlZj0iL3N0YXRpYy9iODUzNmMzNy9zaGFyZWQuY3NzIiByZWw9InN0eWxlc2hlZXQiPiA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiPiAgPGxpbmsgaHJlZj0iL3N0YXRpYy9kMDVjZDZiYS9yb290LmNzcyIgcmVsPSJzdHlsZXNoZWV0Ij4gPC9oZWFkPiA8Ym9keT4gPHNwYW4gY2xhc3M9ImZpbGxlciB0b3AiPjwvc3Bhbj4gICA8ZGl2IGNsYXNzPSJsb2dvIiB0aXRsZT0iR29vZ2xlIFB1YmxpYyBETlMiPiA8ZGl2IGNsYXNzPSJsb2dvLXRleHQiPjxzcGFuPlB1YmxpYyBETlM8L3NwYW4+PC9kaXY+IDwvZGl2PiAgPGZvcm0gYWN0aW9uPSIvcXVlcnkiIG1ldGhvZD0iR0VUIj4gIDxkaXYgY2xhc3M9InJvdyI+IDxsYWJlbCBjbGFzcz0ibWF0dGVyLXRleHRmaWVsZC1vdXRsaW5lZCI+IDxpbnB1dCB0eXBlPSJ0ZXh0IiBuYW1lPSJuYW1lIiBwbGFjZWhvbGRlcj0iJm5ic3A7Ij4gPHNwYW4+RE5TIE5hbWU8L3NwYW4+IDxwIGNsYXNzPSJoZWxwIj4gRW50ZXIgYSBkb21haW4gKGxpa2UgZXhhbXBsZS5jb20pIG9yIElQIGFkZHJlc3MgKGxpa2UgOC44LjguOCBvciAyMDAxOjQ4NjA6NDg2MDo6ODg0NCkgaGVyZS4gPC9wPiA8L2xhYmVsPiA8YnV0dG9uIGNsYXNzPSJtYXR0ZXItYnV0dG9uLWNvbnRhaW5lZCBtYXR0ZXItcHJpbWFyeSIgdHlwZT0ic3VibWl0Ij5SZXNvbHZlPC9idXR0b24+IDwvZGl2PiA8L2Zvcm0+ICA8c3BhbiBjbGFzcz0iZmlsbGVyIGJvdHRvbSI+PC9zcGFuPiA8Zm9vdGVyIGNsYXNzPSJyb3ciPiA8YSBocmVmPSJodHRwczovL2RldmVsb3BlcnMuZ29vZ2xlLmNvbS9zcGVlZC9wdWJsaWMtZG5zIj5IZWxwPC9hPiA8YSBocmVmPSIvY2FjaGUiPkNhY2hlIEZsdXNoPC9hPiA8c3BhbiBjbGFzcz0iZmlsbGVyIj48L3NwYW4+IDxhIGhyZWY9Imh0dHBzOi8vZGV2ZWxvcGVycy5nb29nbGUuY29tL3NwZWVkL3B1YmxpYy1kbnMvZG9jcy91c2luZyI+IEdldCBTdGFydGVkIHdpdGggR29vZ2xlIFB1YmxpYyBETlMgPC9hPiA8L2Zvb3Rlcj4gICA8c2NyaXB0IG5vbmNlPSJiU0xjSmphb3RwcFpsM1kybW9JYXhnPT0iPmRvY3VtZW50LmZvcm1zWzBdLm5hbWUuZm9jdXMoKTs8L3NjcmlwdD4gPC9ib2R5PiA8L2h0bWw+",
|
||||||
|
// "format": "base64"
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// // This field tells us whether the size of the read
|
||||||
|
// // snapshot was smaller than the snapshot size. If
|
||||||
|
// // not, then the body has been truncated.
|
||||||
|
// "body_is_truncated": false,
|
||||||
|
//
|
||||||
|
// // These extra fields are not part of the spec and
|
||||||
|
// // hence we prefix them with `x_`. They tell us
|
||||||
|
// // the length of the body and whether the content
|
||||||
|
// // of the body is valid UTF8.
|
||||||
|
// "x_body_length": 1383,
|
||||||
|
// "x_body_is_utf8": true
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// // The t field is the moment where we finished the
|
||||||
|
// // round trip and saved the event. The started field
|
||||||
|
// // is instead when we started the round trip.
|
||||||
|
//
|
||||||
|
// // You may notice that the start of the round trip
|
||||||
|
// // if after the `t` of the handshake. This tells us
|
||||||
|
// // that the code first connects, then handshakes, and
|
||||||
|
// // finally creates HTTP code for performing the
|
||||||
|
// // round trip.
|
||||||
|
// "t": 0.338674625,
|
||||||
|
// "started": 0.065926625,
|
||||||
|
//
|
||||||
|
// // As usual we also compute an oddity value related
|
||||||
|
// // in this case to the HTTP round trip.
|
||||||
|
// "oddity": ""
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Here are some suggestions for follow up measurements:
|
||||||
|
//
|
||||||
|
// 1. provoke a connect error by using:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter06 -address 127.0.0.1:1
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// 2. provoke a TLS handshake error by using:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter06 -sni example.com
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// 3. provoke an HTTP round trip error by using:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter06 -address 8.8.8.8:853
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// 4. modify the code to fetch an HTTP endpoint instead (hint: you
|
||||||
|
// need to change the HTTPEndpoint's URL scheme);
|
||||||
|
//
|
||||||
|
// 5. modify the code to use QUIC and HTTP/3 instead (hint: you need to
|
||||||
|
// change the HTTPEndpoint's network and... is this enough?).
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have seen how to measure the flow of fetching a
|
||||||
|
// specific webpage from an HTTPEndpoint.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
142
internal/tutorial/measurex/chapter07/README.md
Normal file
142
internal/tutorial/measurex/chapter07/README.md
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
|
||||||
|
# Chapter VII: Measuring all the HTTPEndpoints for a domain
|
||||||
|
|
||||||
|
We are now going to combine DNS resolutions with getting
|
||||||
|
HTTPEndpoints. Conceptually, the DNS resolution yields
|
||||||
|
us a list of IP addresses. For each address, we build the
|
||||||
|
HTTPEndpoint and fetch it like we did in chapter06.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter07/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
We have package declaration and imports as usual.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we define an helper type for containing the DNS
|
||||||
|
measurement and the subsequent endpoints measurements.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
type measurement struct {
|
||||||
|
DNS *measurex.DNSMeasurement
|
||||||
|
Endpoints []*measurex.HTTPEndpointMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The rest of the program is quite similar to what we had before.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://google.com/", "URL to fetch")
|
||||||
|
address := flag.String("address", "8.8.4.4:53", "DNS-over-UDP server address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
parsed, err := url.Parse(*URL)
|
||||||
|
runtimex.PanicOnError(err, "url.Parse failed")
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
```
|
||||||
|
|
||||||
|
This is where the main.go file starts to diverge. We create an
|
||||||
|
instance of our measurement type to hold the results.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m := &measurement{}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we perform a DNS lookup using UDP like we saw in chapter03.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m.DNS = mx.LookupHostUDP(ctx, parsed.Hostname(), *address)
|
||||||
|
```
|
||||||
|
|
||||||
|
Like we did in the previous chapter, we create suitable HTTP
|
||||||
|
headers for performing an HTTP measurement.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
```
|
||||||
|
|
||||||
|
The following is an entirely new function we're learning
|
||||||
|
about just now. `AllHTTPEndpointsForURL` is a free function
|
||||||
|
in `measurex` that given:
|
||||||
|
|
||||||
|
- an already parsed HTTP/HTTPS URL
|
||||||
|
|
||||||
|
- headers we want to use
|
||||||
|
|
||||||
|
- the result of one or more DNS queries
|
||||||
|
|
||||||
|
builds us a list of HTTPEndpoint data structures.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
httpEndpoints, err := measurex.AllHTTPEndpointsForURL(parsed, headers, m.DNS)
|
||||||
|
runtimex.PanicOnError(err, "cannot get all the HTTP endpoints")
|
||||||
|
```
|
||||||
|
|
||||||
|
This function may fail if, for example, the URL is not HTTP/HTTPS. We
|
||||||
|
handle the error panicking, because this is an example program.
|
||||||
|
|
||||||
|
We are almost done now: we loop over all the endpoints and apply the
|
||||||
|
`HTTPEndpointGetWithoutCookies` method we have seen in chapter06.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
for _, epnt := range httpEndpoints {
|
||||||
|
m.Endpoints = append(m.Endpoints, mx.HTTPEndpointGetWithoutCookies(ctx, epnt))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we print the results.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us perform a vanilla run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter07
|
||||||
|
```
|
||||||
|
|
||||||
|
Please, check the JSON output. Do you recognize the fields
|
||||||
|
we have described in previous chapters?
|
||||||
|
|
||||||
|
Can you provoke common errors such as DNS resolution
|
||||||
|
errors, TCP connect errors, TLS handshake errors, and
|
||||||
|
HTTP round trip errors? How does the JSON change?
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have seen how to combine DNS resolutions (chapter01 and
|
||||||
|
chapter03) with HTTPEndpoint GET (chapter06) to measure
|
||||||
|
all the HTTP endpoints for a given domain.
|
||||||
|
|
144
internal/tutorial/measurex/chapter07/main.go
Normal file
144
internal/tutorial/measurex/chapter07/main.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter VII: Measuring all the HTTPEndpoints for a domain
|
||||||
|
//
|
||||||
|
// We are now going to combine DNS resolutions with getting
|
||||||
|
// HTTPEndpoints. Conceptually, the DNS resolution yields
|
||||||
|
// us a list of IP addresses. For each address, we build the
|
||||||
|
// HTTPEndpoint and fetch it like we did in chapter06.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter07/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// We have package declaration and imports as usual.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Here we define an helper type for containing the DNS
|
||||||
|
// measurement and the subsequent endpoints measurements.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
type measurement struct {
|
||||||
|
DNS *measurex.DNSMeasurement
|
||||||
|
Endpoints []*measurex.HTTPEndpointMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The rest of the program is quite similar to what we had before.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://google.com/", "URL to fetch")
|
||||||
|
address := flag.String("address", "8.8.4.4:53", "DNS-over-UDP server address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
parsed, err := url.Parse(*URL)
|
||||||
|
runtimex.PanicOnError(err, "url.Parse failed")
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is where the main.go file starts to diverge. We create an
|
||||||
|
// instance of our measurement type to hold the results.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m := &measurement{}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Then we perform a DNS lookup using UDP like we saw in chapter03.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m.DNS = mx.LookupHostUDP(ctx, parsed.Hostname(), *address)
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Like we did in the previous chapter, we create suitable HTTP
|
||||||
|
// headers for performing an HTTP measurement.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The following is an entirely new function we're learning
|
||||||
|
// about just now. `AllHTTPEndpointsForURL` is a free function
|
||||||
|
// in `measurex` that given:
|
||||||
|
//
|
||||||
|
// - an already parsed HTTP/HTTPS URL
|
||||||
|
//
|
||||||
|
// - headers we want to use
|
||||||
|
//
|
||||||
|
// - the result of one or more DNS queries
|
||||||
|
//
|
||||||
|
// builds us a list of HTTPEndpoint data structures.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
httpEndpoints, err := measurex.AllHTTPEndpointsForURL(parsed, headers, m.DNS)
|
||||||
|
runtimex.PanicOnError(err, "cannot get all the HTTP endpoints")
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This function may fail if, for example, the URL is not HTTP/HTTPS. We
|
||||||
|
// handle the error panicking, because this is an example program.
|
||||||
|
//
|
||||||
|
// We are almost done now: we loop over all the endpoints and apply the
|
||||||
|
// `HTTPEndpointGetWithoutCookies` method we have seen in chapter06.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
for _, epnt := range httpEndpoints {
|
||||||
|
m.Endpoints = append(m.Endpoints, mx.HTTPEndpointGetWithoutCookies(ctx, epnt))
|
||||||
|
}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Finally, we print the results.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us perform a vanilla run first:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter07
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Please, check the JSON output. Do you recognize the fields
|
||||||
|
// we have described in previous chapters?
|
||||||
|
//
|
||||||
|
// Can you provoke common errors such as DNS resolution
|
||||||
|
// errors, TCP connect errors, TLS handshake errors, and
|
||||||
|
// HTTP round trip errors? How does the JSON change?
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have seen how to combine DNS resolutions (chapter01 and
|
||||||
|
// chapter03) with HTTPEndpoint GET (chapter06) to measure
|
||||||
|
// all the HTTP endpoints for a given domain.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
132
internal/tutorial/measurex/chapter08/README.md
Normal file
132
internal/tutorial/measurex/chapter08/README.md
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
|
||||||
|
# Chapter VII: HTTPSSvc DNS queries
|
||||||
|
|
||||||
|
The program we see here is _really_ similar to the one we
|
||||||
|
discussed in the previous chapter. The main difference
|
||||||
|
is the following: now we also issue HTTPSSvc DNS queries
|
||||||
|
to discover HTTP/3 endpoints. (Because HTTPSSvc is
|
||||||
|
still a draft and is mostly implemented by Cloudflare
|
||||||
|
at this point, we are going to use as the example
|
||||||
|
input URL a Cloudflare URL.)
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter08/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The beginning of the program is pretty much the same. We
|
||||||
|
have just amended our `measurement` type to contain multiple
|
||||||
|
`DNSMeasurement` results.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
DNS []*measurex.DNSMeasurement
|
||||||
|
Endpoints []*measurex.HTTPEndpointMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://blog.cloudflare.com/", "URL to fetch")
|
||||||
|
address := flag.String("address", "8.8.4.4:53", "DNS-over-UDP server address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
parsed, err := url.Parse(*URL)
|
||||||
|
runtimex.PanicOnError(err, "url.Parse failed")
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
m := &measurement{}
|
||||||
|
```
|
||||||
|
### Call LookupHTTPSSvc
|
||||||
|
|
||||||
|
Here we perform the `LookupHostUDP` we performed in the
|
||||||
|
previous chapter and then we call `LookupHTTPSvcUDP`.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m.DNS = append(m.DNS, mx.LookupHostUDP(ctx, parsed.Hostname(), *address))
|
||||||
|
m.DNS = append(m.DNS, mx.LookupHTTPSSvcUDP(ctx, parsed.Hostname(), *address))
|
||||||
|
```
|
||||||
|
|
||||||
|
The `LookupHTTPSSvcUDP` function has the same signature
|
||||||
|
of `LookupHostUDP` _but_ it behaves differently. Rather than
|
||||||
|
querying for `A` and `AAAA`, it performs an `HTTPS` DNS
|
||||||
|
lookup. This query returns:
|
||||||
|
|
||||||
|
1. a list of ALPNs for the domain;
|
||||||
|
|
||||||
|
2. a list of IPv4 addresses;
|
||||||
|
|
||||||
|
3. a list of IPv6 addresses.
|
||||||
|
|
||||||
|
### Build an []HTTPEndpoint and run serial measurements
|
||||||
|
|
||||||
|
Here we call `AllHTTPEndpointsForURL` like we did in the
|
||||||
|
previous chapter. However, note that we pass to it the
|
||||||
|
whole content of `m.DNS`, which now contains not only the
|
||||||
|
A/AAAA lookups results but also the HTTPS lookup results.
|
||||||
|
|
||||||
|
The `AllHTTPEndpointsForURL` function will recognize that
|
||||||
|
we also have HTTPS lookups and, if the "h3" ALPN is
|
||||||
|
present, will _also_ build HTTP/3 endpoints using "quic"
|
||||||
|
as the `HTTPEndpoint.Network`.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
httpEndpoints, err := measurex.AllHTTPEndpointsForURL(parsed, headers, m.DNS...)
|
||||||
|
runtimex.PanicOnError(err, "cannot get all the HTTP endpoints")
|
||||||
|
```
|
||||||
|
|
||||||
|
This is it. The rest of the program is exactly the same.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
for _, epnt := range httpEndpoints {
|
||||||
|
m.Endpoints = append(m.Endpoints, mx.HTTPEndpointGetWithoutCookies(ctx, epnt))
|
||||||
|
}
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us perform a vanilla run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter08
|
||||||
|
```
|
||||||
|
|
||||||
|
Please, check the JSON output. Do you recognize the fields
|
||||||
|
we have described in previous chapters? You should see
|
||||||
|
that, compared to previous chapters, now we're also testing
|
||||||
|
QUIC/HTTP3 endpoints.
|
||||||
|
|
||||||
|
Can you provoke common errors such as DNS resolution
|
||||||
|
errors, TCP connect errors, TLS handshake errors, and
|
||||||
|
HTTP round trip errors? What is a good way to cause
|
||||||
|
timeout and SNI mismatch errors for QUIC?
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have seen how to extend fetching all the HTTPS
|
||||||
|
endpoints to include the QUIC/HTTP3 endpoints discovered
|
||||||
|
using HTTPSSvc.
|
||||||
|
|
134
internal/tutorial/measurex/chapter08/main.go
Normal file
134
internal/tutorial/measurex/chapter08/main.go
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter VII: HTTPSSvc DNS queries
|
||||||
|
//
|
||||||
|
// The program we see here is _really_ similar to the one we
|
||||||
|
// discussed in the previous chapter. The main difference
|
||||||
|
// is the following: now we also issue HTTPSSvc DNS queries
|
||||||
|
// to discover HTTP/3 endpoints. (Because HTTPSSvc is
|
||||||
|
// still a draft and is mostly implemented by Cloudflare
|
||||||
|
// at this point, we are going to use as the example
|
||||||
|
// input URL a Cloudflare URL.)
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter08/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The beginning of the program is pretty much the same. We
|
||||||
|
// have just amended our `measurement` type to contain multiple
|
||||||
|
// `DNSMeasurement` results.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
DNS []*measurex.DNSMeasurement
|
||||||
|
Endpoints []*measurex.HTTPEndpointMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://blog.cloudflare.com/", "URL to fetch")
|
||||||
|
address := flag.String("address", "8.8.4.4:53", "DNS-over-UDP server address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
parsed, err := url.Parse(*URL)
|
||||||
|
runtimex.PanicOnError(err, "url.Parse failed")
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
m := &measurement{}
|
||||||
|
// ```
|
||||||
|
// ### Call LookupHTTPSSvc
|
||||||
|
//
|
||||||
|
// Here we perform the `LookupHostUDP` we performed in the
|
||||||
|
// previous chapter and then we call `LookupHTTPSvcUDP`.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m.DNS = append(m.DNS, mx.LookupHostUDP(ctx, parsed.Hostname(), *address))
|
||||||
|
m.DNS = append(m.DNS, mx.LookupHTTPSSvcUDP(ctx, parsed.Hostname(), *address))
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The `LookupHTTPSSvcUDP` function has the same signature
|
||||||
|
// of `LookupHostUDP` _but_ it behaves differently. Rather than
|
||||||
|
// querying for `A` and `AAAA`, it performs an `HTTPS` DNS
|
||||||
|
// lookup. This query returns:
|
||||||
|
//
|
||||||
|
// 1. a list of ALPNs for the domain;
|
||||||
|
//
|
||||||
|
// 2. a list of IPv4 addresses;
|
||||||
|
//
|
||||||
|
// 3. a list of IPv6 addresses.
|
||||||
|
//
|
||||||
|
// ### Build an []HTTPEndpoint and run serial measurements
|
||||||
|
//
|
||||||
|
// Here we call `AllHTTPEndpointsForURL` like we did in the
|
||||||
|
// previous chapter. However, note that we pass to it the
|
||||||
|
// whole content of `m.DNS`, which now contains not only the
|
||||||
|
// A/AAAA lookups results but also the HTTPS lookup results.
|
||||||
|
//
|
||||||
|
// The `AllHTTPEndpointsForURL` function will recognize that
|
||||||
|
// we also have HTTPS lookups and, if the "h3" ALPN is
|
||||||
|
// present, will _also_ build HTTP/3 endpoints using "quic"
|
||||||
|
// as the `HTTPEndpoint.Network`.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
httpEndpoints, err := measurex.AllHTTPEndpointsForURL(parsed, headers, m.DNS...)
|
||||||
|
runtimex.PanicOnError(err, "cannot get all the HTTP endpoints")
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is it. The rest of the program is exactly the same.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
for _, epnt := range httpEndpoints {
|
||||||
|
m.Endpoints = append(m.Endpoints, mx.HTTPEndpointGetWithoutCookies(ctx, epnt))
|
||||||
|
}
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us perform a vanilla run first:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter08
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Please, check the JSON output. Do you recognize the fields
|
||||||
|
// we have described in previous chapters? You should see
|
||||||
|
// that, compared to previous chapters, now we're also testing
|
||||||
|
// QUIC/HTTP3 endpoints.
|
||||||
|
//
|
||||||
|
// Can you provoke common errors such as DNS resolution
|
||||||
|
// errors, TCP connect errors, TLS handshake errors, and
|
||||||
|
// HTTP round trip errors? What is a good way to cause
|
||||||
|
// timeout and SNI mismatch errors for QUIC?
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have seen how to extend fetching all the HTTPS
|
||||||
|
// endpoints to include the QUIC/HTTP3 endpoints discovered
|
||||||
|
// using HTTPSSvc.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
124
internal/tutorial/measurex/chapter09/README.md
Normal file
124
internal/tutorial/measurex/chapter09/README.md
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
|
||||||
|
# Chapter IX: Parallel HTTPEndpoint measurements
|
||||||
|
|
||||||
|
The program we see here is _really_ similar to the one we
|
||||||
|
discussed in the previous chapter. The main difference
|
||||||
|
is the following: rather than looping through the list of
|
||||||
|
HTTPEndpoint, we call a function that runs through the
|
||||||
|
list of endpoints using a small pool of background workers.
|
||||||
|
|
||||||
|
There is a trade off between quick measurements and
|
||||||
|
false positives. A timeout is one of the most common
|
||||||
|
ways of censoring HTTPS and HTTP3 endpoints. So, if
|
||||||
|
we run measurements sequentially, a whole scan could
|
||||||
|
in principle take a long time. On the other hand,
|
||||||
|
if we run too many parallel measurements, we may cause
|
||||||
|
our own congestion and maybe some measurements will
|
||||||
|
fail because of that. Our solution to this problem is
|
||||||
|
to have low parallelism: at the moment of writing
|
||||||
|
this note, we have three workers. If you submit
|
||||||
|
more than three HTTPEndpoint at a a time, we will
|
||||||
|
service the first three immediately and all the
|
||||||
|
other endpoints will be queued for later measurement.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter09/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The beginning of the program is pretty much the same.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
DNS []*measurex.DNSMeasurement
|
||||||
|
Endpoints []*measurex.HTTPEndpointMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://blog.cloudflare.com/", "URL to fetch")
|
||||||
|
address := flag.String("address", "8.8.4.4:53", "DNS-over-UDP server address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
parsed, err := url.Parse(*URL)
|
||||||
|
runtimex.PanicOnError(err, "url.Parse failed")
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
m := &measurement{}
|
||||||
|
m.DNS = append(m.DNS, mx.LookupHostUDP(ctx, parsed.Hostname(), *address))
|
||||||
|
m.DNS = append(m.DNS, mx.LookupHTTPSSvcUDP(ctx, parsed.Hostname(), *address))
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
httpEndpoints, err := measurex.AllHTTPEndpointsForURL(parsed, headers, m.DNS...)
|
||||||
|
runtimex.PanicOnError(err, "cannot get all the HTTP endpoints")
|
||||||
|
```
|
||||||
|
|
||||||
|
This is where the program changes. First, we need to create a jar
|
||||||
|
for cookies because the API we're about to call requires a
|
||||||
|
cookie jar. (We mostly use this API with redirects and we want
|
||||||
|
to have cookies with redirects because a small portion of the
|
||||||
|
URLs we typically test require cookies to properly redirect,
|
||||||
|
see https://github.com/ooni/probe/issues/1727 for more information).
|
||||||
|
|
||||||
|
Then, we call `HTTPEndpointGetParallel`. The arguments are:
|
||||||
|
|
||||||
|
- as usual, the context
|
||||||
|
|
||||||
|
- the cookie jar
|
||||||
|
|
||||||
|
- all the endpoints to measure
|
||||||
|
|
||||||
|
```Go
|
||||||
|
cookies := measurex.NewCookieJar()
|
||||||
|
for epnt := range mx.HTTPEndpointGetParallel(ctx, cookies, httpEndpoints...) {
|
||||||
|
m.Endpoints = append(m.Endpoints, epnt)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `HTTPEndpointGetParallel` method returns a channel where it
|
||||||
|
posts `HTTPEndpointMeasurements`. Once the input list has been
|
||||||
|
fully measured, this method closes the returned channel.
|
||||||
|
|
||||||
|
Like we did before, we append the resulting measurements to
|
||||||
|
our `m` container and we print it.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us perform a vanilla run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter09
|
||||||
|
```
|
||||||
|
|
||||||
|
Take a look at the JSON output. Can you spot that
|
||||||
|
endpoints measurements are run in parallel?
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have seen how to run HTTPEndpoint measurements in parallel.
|
||||||
|
|
126
internal/tutorial/measurex/chapter09/main.go
Normal file
126
internal/tutorial/measurex/chapter09/main.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter IX: Parallel HTTPEndpoint measurements
|
||||||
|
//
|
||||||
|
// The program we see here is _really_ similar to the one we
|
||||||
|
// discussed in the previous chapter. The main difference
|
||||||
|
// is the following: rather than looping through the list of
|
||||||
|
// HTTPEndpoint, we call a function that runs through the
|
||||||
|
// list of endpoints using a small pool of background workers.
|
||||||
|
//
|
||||||
|
// There is a trade off between quick measurements and
|
||||||
|
// false positives. A timeout is one of the most common
|
||||||
|
// ways of censoring HTTPS and HTTP3 endpoints. So, if
|
||||||
|
// we run measurements sequentially, a whole scan could
|
||||||
|
// in principle take a long time. On the other hand,
|
||||||
|
// if we run too many parallel measurements, we may cause
|
||||||
|
// our own congestion and maybe some measurements will
|
||||||
|
// fail because of that. Our solution to this problem is
|
||||||
|
// to have low parallelism: at the moment of writing
|
||||||
|
// this note, we have three workers. If you submit
|
||||||
|
// more than three HTTPEndpoint at a a time, we will
|
||||||
|
// service the first three immediately and all the
|
||||||
|
// other endpoints will be queued for later measurement.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter09/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The beginning of the program is pretty much the same.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
DNS []*measurex.DNSMeasurement
|
||||||
|
Endpoints []*measurex.HTTPEndpointMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://blog.cloudflare.com/", "URL to fetch")
|
||||||
|
address := flag.String("address", "8.8.4.4:53", "DNS-over-UDP server address")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
parsed, err := url.Parse(*URL)
|
||||||
|
runtimex.PanicOnError(err, "url.Parse failed")
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
m := &measurement{}
|
||||||
|
m.DNS = append(m.DNS, mx.LookupHostUDP(ctx, parsed.Hostname(), *address))
|
||||||
|
m.DNS = append(m.DNS, mx.LookupHTTPSSvcUDP(ctx, parsed.Hostname(), *address))
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
httpEndpoints, err := measurex.AllHTTPEndpointsForURL(parsed, headers, m.DNS...)
|
||||||
|
runtimex.PanicOnError(err, "cannot get all the HTTP endpoints")
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is where the program changes. First, we need to create a jar
|
||||||
|
// for cookies because the API we're about to call requires a
|
||||||
|
// cookie jar. (We mostly use this API with redirects and we want
|
||||||
|
// to have cookies with redirects because a small portion of the
|
||||||
|
// URLs we typically test require cookies to properly redirect,
|
||||||
|
// see https://github.com/ooni/probe/issues/1727 for more information).
|
||||||
|
//
|
||||||
|
// Then, we call `HTTPEndpointGetParallel`. The arguments are:
|
||||||
|
//
|
||||||
|
// - as usual, the context
|
||||||
|
//
|
||||||
|
// - the cookie jar
|
||||||
|
//
|
||||||
|
// - all the endpoints to measure
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
cookies := measurex.NewCookieJar()
|
||||||
|
for epnt := range mx.HTTPEndpointGetParallel(ctx, cookies, httpEndpoints...) {
|
||||||
|
m.Endpoints = append(m.Endpoints, epnt)
|
||||||
|
}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The `HTTPEndpointGetParallel` method returns a channel where it
|
||||||
|
// posts `HTTPEndpointMeasurements`. Once the input list has been
|
||||||
|
// fully measured, this method closes the returned channel.
|
||||||
|
//
|
||||||
|
// Like we did before, we append the resulting measurements to
|
||||||
|
// our `m` container and we print it.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us perform a vanilla run first:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter09
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Take a look at the JSON output. Can you spot that
|
||||||
|
// endpoints measurements are run in parallel?
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have seen how to run HTTPEndpoint measurements in parallel.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
127
internal/tutorial/measurex/chapter10/README.md
Normal file
127
internal/tutorial/measurex/chapter10/README.md
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
|
||||||
|
# Chapter IX: Parallel DNS lookups
|
||||||
|
|
||||||
|
The program we see here is _really_ similar to the one we
|
||||||
|
discussed in the previous chapter. The main difference
|
||||||
|
is the following: rather than performing DNS lookups
|
||||||
|
sequentially, we call a function that runs through the
|
||||||
|
list of resolvers and run them in parallel.
|
||||||
|
|
||||||
|
Again, we are going to use low parallelism for the same
|
||||||
|
rationale mentioned in chapter09.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter10/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The beginning of the program is pretty much the same.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
DNS []*measurex.DNSMeasurement
|
||||||
|
Endpoints []*measurex.HTTPEndpointMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://blog.cloudflare.com/", "URL to fetch")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
parsed, err := url.Parse(*URL)
|
||||||
|
runtimex.PanicOnError(err, "url.Parse failed")
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
m := &measurement{}
|
||||||
|
```
|
||||||
|
|
||||||
|
The bulk of the difference is here. We create
|
||||||
|
a list of DNS resolvers. For each of them, we specify
|
||||||
|
the type and the endpoint address. (There is no
|
||||||
|
endpoint address for the system resolver, therefore
|
||||||
|
we leave its address empty.)
|
||||||
|
|
||||||
|
```Go
|
||||||
|
resolvers := []*measurex.ResolverInfo{{
|
||||||
|
Network: measurex.ResolverUDP,
|
||||||
|
Address: "8.8.8.8:53",
|
||||||
|
}, {
|
||||||
|
Network: measurex.ResolverUDP,
|
||||||
|
Address: "8.8.4.4:53",
|
||||||
|
}, {
|
||||||
|
Network: measurex.ResolverUDP,
|
||||||
|
Address: "1.1.1.1:53",
|
||||||
|
}, {
|
||||||
|
Network: measurex.ResolverUDP,
|
||||||
|
Address: "1.0.0.1:53",
|
||||||
|
}, {
|
||||||
|
Network: measurex.ResolverSystem,
|
||||||
|
Address: "",
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we call `LookupURLHostParallel`. This function runs
|
||||||
|
the queries that make sense given the input URL using a
|
||||||
|
pool of (currently three) background goroutines.
|
||||||
|
|
||||||
|
When I say "queries that make sense", I mostly mean
|
||||||
|
that we only query for HTTPSSvc when the input URL
|
||||||
|
scheme is "https". Otherwise, if it's just "http", it
|
||||||
|
does not make sense to send this query.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
for dns := range mx.LookupURLHostParallel(ctx, parsed, resolvers...) {
|
||||||
|
m.DNS = append(m.DNS, dns)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The rest of the program is exactly like in chapter09.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
httpEndpoints, err := measurex.AllHTTPEndpointsForURL(parsed, headers, m.DNS...)
|
||||||
|
runtimex.PanicOnError(err, "cannot get all the HTTP endpoints")
|
||||||
|
cookies := measurex.NewCookieJar()
|
||||||
|
for epnt := range mx.HTTPEndpointGetParallel(ctx, cookies, httpEndpoints...) {
|
||||||
|
m.Endpoints = append(m.Endpoints, epnt)
|
||||||
|
}
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us perform a vanilla run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter10
|
||||||
|
```
|
||||||
|
|
||||||
|
Take a look at the JSON output. Can you spot that
|
||||||
|
DNS queries are run in parallel?
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have seen how to run parallel DNS queries.
|
||||||
|
|
129
internal/tutorial/measurex/chapter10/main.go
Normal file
129
internal/tutorial/measurex/chapter10/main.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter IX: Parallel DNS lookups
|
||||||
|
//
|
||||||
|
// The program we see here is _really_ similar to the one we
|
||||||
|
// discussed in the previous chapter. The main difference
|
||||||
|
// is the following: rather than performing DNS lookups
|
||||||
|
// sequentially, we call a function that runs through the
|
||||||
|
// list of resolvers and run them in parallel.
|
||||||
|
//
|
||||||
|
// Again, we are going to use low parallelism for the same
|
||||||
|
// rationale mentioned in chapter09.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter10/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The beginning of the program is pretty much the same.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
DNS []*measurex.DNSMeasurement
|
||||||
|
Endpoints []*measurex.HTTPEndpointMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://blog.cloudflare.com/", "URL to fetch")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
parsed, err := url.Parse(*URL)
|
||||||
|
runtimex.PanicOnError(err, "url.Parse failed")
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
m := &measurement{}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The bulk of the difference is here. We create
|
||||||
|
// a list of DNS resolvers. For each of them, we specify
|
||||||
|
// the type and the endpoint address. (There is no
|
||||||
|
// endpoint address for the system resolver, therefore
|
||||||
|
// we leave its address empty.)
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
resolvers := []*measurex.ResolverInfo{{
|
||||||
|
Network: measurex.ResolverUDP,
|
||||||
|
Address: "8.8.8.8:53",
|
||||||
|
}, {
|
||||||
|
Network: measurex.ResolverUDP,
|
||||||
|
Address: "8.8.4.4:53",
|
||||||
|
}, {
|
||||||
|
Network: measurex.ResolverUDP,
|
||||||
|
Address: "1.1.1.1:53",
|
||||||
|
}, {
|
||||||
|
Network: measurex.ResolverUDP,
|
||||||
|
Address: "1.0.0.1:53",
|
||||||
|
}, {
|
||||||
|
Network: measurex.ResolverSystem,
|
||||||
|
Address: "",
|
||||||
|
}}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Then we call `LookupURLHostParallel`. This function runs
|
||||||
|
// the queries that make sense given the input URL using a
|
||||||
|
// pool of (currently three) background goroutines.
|
||||||
|
//
|
||||||
|
// When I say "queries that make sense", I mostly mean
|
||||||
|
// that we only query for HTTPSSvc when the input URL
|
||||||
|
// scheme is "https". Otherwise, if it's just "http", it
|
||||||
|
// does not make sense to send this query.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
for dns := range mx.LookupURLHostParallel(ctx, parsed, resolvers...) {
|
||||||
|
m.DNS = append(m.DNS, dns)
|
||||||
|
}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The rest of the program is exactly like in chapter09.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
httpEndpoints, err := measurex.AllHTTPEndpointsForURL(parsed, headers, m.DNS...)
|
||||||
|
runtimex.PanicOnError(err, "cannot get all the HTTP endpoints")
|
||||||
|
cookies := measurex.NewCookieJar()
|
||||||
|
for epnt := range mx.HTTPEndpointGetParallel(ctx, cookies, httpEndpoints...) {
|
||||||
|
m.Endpoints = append(m.Endpoints, epnt)
|
||||||
|
}
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us perform a vanilla run first:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter10
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Take a look at the JSON output. Can you spot that
|
||||||
|
// DNS queries are run in parallel?
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have seen how to run parallel DNS queries.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
126
internal/tutorial/measurex/chapter11/README.md
Normal file
126
internal/tutorial/measurex/chapter11/README.md
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
|
||||||
|
# Chapter XI: Measuring a URL
|
||||||
|
|
||||||
|
This program shows how to measure an HTTP/HTTPS URL. We
|
||||||
|
are going to call an API whose implementation is
|
||||||
|
basically the same code we have seen in the previous
|
||||||
|
chapter, to obtain an URL measurement in a more compact
|
||||||
|
way. (As an historical note, the API we are going to
|
||||||
|
call has indeed been written as a refactoring of
|
||||||
|
the code we introduced in the previous chapter.)
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter11/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The beginning of the program is much simpler. We have removed
|
||||||
|
out custom measurement type. We are now going to use the
|
||||||
|
`URLMeasurement` type (`go doc ./internal/measurex.URLMeasurement`),
|
||||||
|
which as the same fields of `measurement` in chapter10 _plus_
|
||||||
|
some extra fields that we'll examine in a later chapter.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://www.google.com/", "URL to fetch")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
```
|
||||||
|
|
||||||
|
We create a measurer, cookies, and headers like we
|
||||||
|
saw in the previous chapter.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
cookies := measurex.NewCookieJar()
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we call `MeasureURL`. This function's implementation
|
||||||
|
is in `./internal/measurex/measurer.go` and is pretty
|
||||||
|
much a refactoring of the code in chapter10.
|
||||||
|
|
||||||
|
The arguments are:
|
||||||
|
|
||||||
|
- the context as usual
|
||||||
|
|
||||||
|
- the unparsed URL to measure
|
||||||
|
|
||||||
|
- the headers we want to use
|
||||||
|
|
||||||
|
- a jar for cookies
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m, err := mx.MeasureURL(ctx, *URL, headers, cookies)
|
||||||
|
```
|
||||||
|
The return value is either an `URLMeasurement`
|
||||||
|
or an error. The error happens, for example, if
|
||||||
|
the input URL scheme is not "http" or "https" (which
|
||||||
|
we handled by panicking in chapter11).
|
||||||
|
|
||||||
|
Now, rather than panicking inside `MeasureURL`, we
|
||||||
|
return the error to the caller and we `panic`
|
||||||
|
here on `main` using the `PanicOnError` function.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
runtimex.PanicOnError(err, "mx.MeasureURL failed")
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us perform a vanilla run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter11
|
||||||
|
```
|
||||||
|
|
||||||
|
Take a look at the JSON output and compare it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter10 -url https://www.google.com
|
||||||
|
```
|
||||||
|
|
||||||
|
(which is basically forcing chapter10 to run with the
|
||||||
|
the default URL we use in this chapter).
|
||||||
|
|
||||||
|
Can you explain why we are able to measure more endpoints
|
||||||
|
in this chapter by checking the implementation of `MeasureURL`
|
||||||
|
and compare it to the code written in chapter10?
|
||||||
|
|
||||||
|
Now run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter11 -url https://google.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Do you see the opportunity there for following redirections? :^).
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have introduced `MeasureURL`, the top-level API for
|
||||||
|
measuring a single URL.
|
||||||
|
|
128
internal/tutorial/measurex/chapter11/main.go
Normal file
128
internal/tutorial/measurex/chapter11/main.go
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter XI: Measuring a URL
|
||||||
|
//
|
||||||
|
// This program shows how to measure an HTTP/HTTPS URL. We
|
||||||
|
// are going to call an API whose implementation is
|
||||||
|
// basically the same code we have seen in the previous
|
||||||
|
// chapter, to obtain an URL measurement in a more compact
|
||||||
|
// way. (As an historical note, the API we are going to
|
||||||
|
// call has indeed been written as a refactoring of
|
||||||
|
// the code we introduced in the previous chapter.)
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter11/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The beginning of the program is much simpler. We have removed
|
||||||
|
// out custom measurement type. We are now going to use the
|
||||||
|
// `URLMeasurement` type (`go doc ./internal/measurex.URLMeasurement`),
|
||||||
|
// which as the same fields of `measurement` in chapter10 _plus_
|
||||||
|
// some extra fields that we'll examine in a later chapter.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://www.google.com/", "URL to fetch")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We create a measurer, cookies, and headers like we
|
||||||
|
// saw in the previous chapter.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
cookies := measurex.NewCookieJar()
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Then we call `MeasureURL`. This function's implementation
|
||||||
|
// is in `./internal/measurex/measurer.go` and is pretty
|
||||||
|
// much a refactoring of the code in chapter10.
|
||||||
|
//
|
||||||
|
// The arguments are:
|
||||||
|
//
|
||||||
|
// - the context as usual
|
||||||
|
//
|
||||||
|
// - the unparsed URL to measure
|
||||||
|
//
|
||||||
|
// - the headers we want to use
|
||||||
|
//
|
||||||
|
// - a jar for cookies
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m, err := mx.MeasureURL(ctx, *URL, headers, cookies)
|
||||||
|
// ```
|
||||||
|
// The return value is either an `URLMeasurement`
|
||||||
|
// or an error. The error happens, for example, if
|
||||||
|
// the input URL scheme is not "http" or "https" (which
|
||||||
|
// we handled by panicking in chapter11).
|
||||||
|
//
|
||||||
|
// Now, rather than panicking inside `MeasureURL`, we
|
||||||
|
// return the error to the caller and we `panic`
|
||||||
|
// here on `main` using the `PanicOnError` function.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
runtimex.PanicOnError(err, "mx.MeasureURL failed")
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us perform a vanilla run first:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter11
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Take a look at the JSON output and compare it with:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter10 -url https://www.google.com
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// (which is basically forcing chapter10 to run with the
|
||||||
|
// the default URL we use in this chapter).
|
||||||
|
//
|
||||||
|
// Can you explain why we are able to measure more endpoints
|
||||||
|
// in this chapter by checking the implementation of `MeasureURL`
|
||||||
|
// and compare it to the code written in chapter10?
|
||||||
|
//
|
||||||
|
// Now run:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter11 -url https://google.com
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Do you see the opportunity there for following redirections? :^).
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have introduced `MeasureURL`, the top-level API for
|
||||||
|
// measuring a single URL.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
94
internal/tutorial/measurex/chapter12/README.md
Normal file
94
internal/tutorial/measurex/chapter12/README.md
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
|
||||||
|
# Chapter XII: Following redirections.
|
||||||
|
|
||||||
|
This program shows how to combine the URL measurement
|
||||||
|
"step" introduced in the previous chapter with
|
||||||
|
following redirections. If we say that the previous
|
||||||
|
chapter performed a "web step", then we can say
|
||||||
|
that here we're performing multiple "web steps".
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter12/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The beginning of the program is pretty much the
|
||||||
|
same, except that here we need to define a
|
||||||
|
`measurement` container type that will contain
|
||||||
|
the result of each "web step".
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
URLs []*measurex.URLMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "http://facebook.com/", "URL to fetch")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
all := &measurement{}
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
cookies := measurex.NewCookieJar()
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything above this line is like in chapter11. What changes
|
||||||
|
now is that we're calling `MeasureURLAndFollowRedirections`
|
||||||
|
instead of `MeasureURL`.
|
||||||
|
|
||||||
|
Rather than returning a single measurement, this function
|
||||||
|
returns a channel where it posts the result of measuring
|
||||||
|
the original URL along with all its redirections. Internally,
|
||||||
|
`MeasureURLAndFollowRedirections` calls `MeasureURL`.
|
||||||
|
|
||||||
|
We accumulate the results in `URLs` and print `m`. The channel
|
||||||
|
is closed when done by `MeasureURLAndFollowRedirections`, so we leave the loop.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
for m := range mx.MeasureURLAndFollowRedirections(ctx, *URL, headers, cookies) {
|
||||||
|
all.URLs = append(all.URLs, m)
|
||||||
|
}
|
||||||
|
print(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us perform a vanilla run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter12
|
||||||
|
```
|
||||||
|
|
||||||
|
Take a look at the JSON. You should see several redirects
|
||||||
|
and that we measure each endpoint of each redirect, including
|
||||||
|
QUIC endpoints that we discover on the way.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have introduced `MeasureURLAndFollowRedirect`, the
|
||||||
|
top-level API for fully measuring a URL and all the URLs
|
||||||
|
that derive from such an URL via redirection.
|
||||||
|
|
96
internal/tutorial/measurex/chapter12/main.go
Normal file
96
internal/tutorial/measurex/chapter12/main.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter XII: Following redirections.
|
||||||
|
//
|
||||||
|
// This program shows how to combine the URL measurement
|
||||||
|
// "step" introduced in the previous chapter with
|
||||||
|
// following redirections. If we say that the previous
|
||||||
|
// chapter performed a "web step", then we can say
|
||||||
|
// that here we're performing multiple "web steps".
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter12/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The beginning of the program is pretty much the
|
||||||
|
// same, except that here we need to define a
|
||||||
|
// `measurement` container type that will contain
|
||||||
|
// the result of each "web step".
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
URLs []*measurex.URLMeasurement
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "http://facebook.com/", "URL to fetch")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
all := &measurement{}
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
cookies := measurex.NewCookieJar()
|
||||||
|
headers := measurex.NewHTTPRequestHeaderForMeasuring()
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Everything above this line is like in chapter11. What changes
|
||||||
|
// now is that we're calling `MeasureURLAndFollowRedirections`
|
||||||
|
// instead of `MeasureURL`.
|
||||||
|
//
|
||||||
|
// Rather than returning a single measurement, this function
|
||||||
|
// returns a channel where it posts the result of measuring
|
||||||
|
// the original URL along with all its redirections. Internally,
|
||||||
|
// `MeasureURLAndFollowRedirections` calls `MeasureURL`.
|
||||||
|
//
|
||||||
|
// We accumulate the results in `URLs` and print `m`. The channel
|
||||||
|
// is closed when done by `MeasureURLAndFollowRedirections`, so we leave the loop.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
for m := range mx.MeasureURLAndFollowRedirections(ctx, *URL, headers, cookies) {
|
||||||
|
all.URLs = append(all.URLs, m)
|
||||||
|
}
|
||||||
|
print(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us perform a vanilla run first:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter12
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Take a look at the JSON. You should see several redirects
|
||||||
|
// and that we measure each endpoint of each redirect, including
|
||||||
|
// QUIC endpoints that we discover on the way.
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have introduced `MeasureURLAndFollowRedirect`, the
|
||||||
|
// top-level API for fully measuring a URL and all the URLs
|
||||||
|
// that derive from such an URL via redirection.
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
97
internal/tutorial/measurex/chapter13/README.md
Normal file
97
internal/tutorial/measurex/chapter13/README.md
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
|
||||||
|
# Chapter XIII: Rewriting Web Connectivity
|
||||||
|
|
||||||
|
This chapter contains an exercise. We are going to
|
||||||
|
use the `measurex` API to rewrite part of the
|
||||||
|
Web Connectivity network experiment.
|
||||||
|
(This is probably the right place to prod you
|
||||||
|
to go to the [ooni/spec](https://github.com/ooni/spec)
|
||||||
|
repository, locate the ts-017-web-connectivity.md
|
||||||
|
spec, and read it.)
|
||||||
|
|
||||||
|
Read the spec? Good, so
|
||||||
|
what we are more precisely going to do here
|
||||||
|
is implement the network measurement part of
|
||||||
|
Web Connectivity where we:
|
||||||
|
|
||||||
|
1. enumerate all the IP addresses of the target
|
||||||
|
URL using the system resolver;
|
||||||
|
|
||||||
|
2. build endpoints with such IPs with a suitable
|
||||||
|
port, thus obtaining a list of HTTP endpoints;
|
||||||
|
|
||||||
|
3. TCP connect each of the endpoints and save the
|
||||||
|
results into a measurement object compatible
|
||||||
|
with Web Connectivity's data format;
|
||||||
|
|
||||||
|
4. TLS handshake each endpoint (only if this
|
||||||
|
makes sense, of course);
|
||||||
|
|
||||||
|
5. HTTP GET the URL and follow redirects until
|
||||||
|
we reach a webpage, fetch the body, and store it
|
||||||
|
for later analysis (which we'll not implement
|
||||||
|
as part of this exercise).
|
||||||
|
|
||||||
|
Let us now provide extra context that should
|
||||||
|
help you figure out how to solve this exercise.
|
||||||
|
|
||||||
|
## Regarding points 3-4
|
||||||
|
|
||||||
|
You already know all the primitives.
|
||||||
|
|
||||||
|
## Regarding point 5
|
||||||
|
|
||||||
|
Historically this point has always been
|
||||||
|
performed by a separate HTTP client. This
|
||||||
|
means that any implementation:
|
||||||
|
|
||||||
|
- will not include any TCP or TLS event
|
||||||
|
generated during point 5 in the measurement;
|
||||||
|
|
||||||
|
- most likely will resolve the URL's domain
|
||||||
|
again (even though the probe-cli implementation
|
||||||
|
uses a fake Resolver to avoid that);
|
||||||
|
|
||||||
|
- tries every available IP address and stops
|
||||||
|
at the first one to which it can connect to (which
|
||||||
|
is what a naive HTTP client does, whereas a more
|
||||||
|
advanced one likely tries a couple of addrs in
|
||||||
|
parallel, especially when both IPv4 and IPv6
|
||||||
|
are supported - this is also known as happy eyeballs).
|
||||||
|
|
||||||
|
In terms of `measurex`, the best API to do what
|
||||||
|
you're required to do in point 5 is probably
|
||||||
|
`NewTracingHTTPTransportWithDefaultSettings`, which
|
||||||
|
allows you to trace only the HTTP round trip and
|
||||||
|
ignores any other event.
|
||||||
|
|
||||||
|
Once you have such a transport, the best `Measurer`
|
||||||
|
API for the task is probably `HTTPClientGET`.
|
||||||
|
|
||||||
|
## Other remarks
|
||||||
|
|
||||||
|
You also need to learn about how to measure
|
||||||
|
events at low level, which entails creating an
|
||||||
|
instance of `MeasurementDB`, passing it to
|
||||||
|
the relevant networking code, and then calling
|
||||||
|
its `AsMeasurement` method to get back a
|
||||||
|
measurement. (You can probably get an idea
|
||||||
|
of how this is done in general by checking the
|
||||||
|
implementation of `Measurer.TCPConnect`.)
|
||||||
|
|
||||||
|
Hopefully, this should be enough information
|
||||||
|
to help you tackle this task. As you see
|
||||||
|
below, the main function is there empty waiting
|
||||||
|
for your implementation. We will provide our
|
||||||
|
own solution to this problem in the next chapter.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter13/main.go`.)
|
||||||
|
|
||||||
|
## The main.go file
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
}
|
98
internal/tutorial/measurex/chapter13/main.go
Normal file
98
internal/tutorial/measurex/chapter13/main.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter XIII: Rewriting Web Connectivity
|
||||||
|
//
|
||||||
|
// This chapter contains an exercise. We are going to
|
||||||
|
// use the `measurex` API to rewrite part of the
|
||||||
|
// Web Connectivity network experiment.
|
||||||
|
// (This is probably the right place to prod you
|
||||||
|
// to go to the [ooni/spec](https://github.com/ooni/spec)
|
||||||
|
// repository, locate the ts-017-web-connectivity.md
|
||||||
|
// spec, and read it.)
|
||||||
|
//
|
||||||
|
// Read the spec? Good, so
|
||||||
|
// what we are more precisely going to do here
|
||||||
|
// is implement the network measurement part of
|
||||||
|
// Web Connectivity where we:
|
||||||
|
//
|
||||||
|
// 1. enumerate all the IP addresses of the target
|
||||||
|
// URL using the system resolver;
|
||||||
|
//
|
||||||
|
// 2. build endpoints with such IPs with a suitable
|
||||||
|
// port, thus obtaining a list of HTTP endpoints;
|
||||||
|
//
|
||||||
|
// 3. TCP connect each of the endpoints and save the
|
||||||
|
// results into a measurement object compatible
|
||||||
|
// with Web Connectivity's data format;
|
||||||
|
//
|
||||||
|
// 4. TLS handshake each endpoint (only if this
|
||||||
|
// makes sense, of course);
|
||||||
|
//
|
||||||
|
// 5. HTTP GET the URL and follow redirects until
|
||||||
|
// we reach a webpage, fetch the body, and store it
|
||||||
|
// for later analysis (which we'll not implement
|
||||||
|
// as part of this exercise).
|
||||||
|
//
|
||||||
|
// Let us now provide extra context that should
|
||||||
|
// help you figure out how to solve this exercise.
|
||||||
|
//
|
||||||
|
// ## Regarding points 3-4
|
||||||
|
//
|
||||||
|
// You already know all the primitives.
|
||||||
|
//
|
||||||
|
// ## Regarding point 5
|
||||||
|
//
|
||||||
|
// Historically this point has always been
|
||||||
|
// performed by a separate HTTP client. This
|
||||||
|
// means that any implementation:
|
||||||
|
//
|
||||||
|
// - will not include any TCP or TLS event
|
||||||
|
// generated during point 5 in the measurement;
|
||||||
|
//
|
||||||
|
// - most likely will resolve the URL's domain
|
||||||
|
// again (even though the probe-cli implementation
|
||||||
|
// uses a fake Resolver to avoid that);
|
||||||
|
//
|
||||||
|
// - tries every available IP address and stops
|
||||||
|
// at the first one to which it can connect to (which
|
||||||
|
// is what a naive HTTP client does, whereas a more
|
||||||
|
// advanced one likely tries a couple of addrs in
|
||||||
|
// parallel, especially when both IPv4 and IPv6
|
||||||
|
// are supported - this is also known as happy eyeballs).
|
||||||
|
//
|
||||||
|
// In terms of `measurex`, the best API to do what
|
||||||
|
// you're required to do in point 5 is probably
|
||||||
|
// `NewTracingHTTPTransportWithDefaultSettings`, which
|
||||||
|
// allows you to trace only the HTTP round trip and
|
||||||
|
// ignores any other event.
|
||||||
|
//
|
||||||
|
// Once you have such a transport, the best `Measurer`
|
||||||
|
// API for the task is probably `HTTPClientGET`.
|
||||||
|
//
|
||||||
|
// ## Other remarks
|
||||||
|
//
|
||||||
|
// You also need to learn about how to measure
|
||||||
|
// events at low level, which entails creating an
|
||||||
|
// instance of `MeasurementDB`, passing it to
|
||||||
|
// the relevant networking code, and then calling
|
||||||
|
// its `AsMeasurement` method to get back a
|
||||||
|
// measurement. (You can probably get an idea
|
||||||
|
// of how this is done in general by checking the
|
||||||
|
// implementation of `Measurer.TCPConnect`.)
|
||||||
|
//
|
||||||
|
// Hopefully, this should be enough information
|
||||||
|
// to help you tackle this task. As you see
|
||||||
|
// below, the main function is there empty waiting
|
||||||
|
// for your implementation. We will provide our
|
||||||
|
// own solution to this problem in the next chapter.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter13/main.go`.)
|
||||||
|
//
|
||||||
|
// ## The main.go file
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
}
|
318
internal/tutorial/measurex/chapter14/README.md
Normal file
318
internal/tutorial/measurex/chapter14/README.md
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
|
||||||
|
# Chapter XIV: A possible rewrite of Web Connectivity
|
||||||
|
|
||||||
|
In this chapter we try to solve the exercise laid out in
|
||||||
|
the previous chapter, using `measurex` primitives.
|
||||||
|
|
||||||
|
(This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
changes you need to modify `./internal/tutorial/measurex/chapter14/main.go`.)
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
The beginning of the file is always pretty much the same.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## measurement type
|
||||||
|
|
||||||
|
We define a measurement type with the fields
|
||||||
|
that a Web Connectivity measurement should have.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
Queries []*measurex.DNSLookupEvent `json:"queries"`
|
||||||
|
TCPConnect []*measurex.NetworkEvent `json:"tcp_connect"`
|
||||||
|
TLSHandshakes []*measurex.TLSHandshakeEvent `json:"tls_handshakes"`
|
||||||
|
Requests []*measurex.HTTPRoundTripEvent `json:"requests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebConnectivity implementation
|
||||||
|
|
||||||
|
We define a function that takes in input a context and a URL to
|
||||||
|
measure and returns a measurement or an error.
|
||||||
|
|
||||||
|
We will only error out in case the input does not allow us to
|
||||||
|
proceed (i.e., invalid input URL).
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
func webConnectivity(ctx context.Context, URL string) (*measurement, error) {
|
||||||
|
```
|
||||||
|
|
||||||
|
We start by parsing the input URL. If we cannot parse it, of
|
||||||
|
course this is an hard error and we cannot continue.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
parsedURL, err := url.Parse(URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
We create an empty measurement and a measurer with
|
||||||
|
default settings like we did in the previous chapters.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
m := &measurement{}
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now it's time to start measuring. We will address all
|
||||||
|
the points laid out in the previous chapter.
|
||||||
|
|
||||||
|
### 1. Enumerating IP addrs
|
||||||
|
|
||||||
|
Let us enumerate all the IP addresses for
|
||||||
|
the input URL's domain using the system resolver.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
dns := mx.LookupHostSystem(ctx, parsedURL.Hostname())
|
||||||
|
m.Queries = append(m.Queries, dns.LookupHost...)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
This is code we have already seen in previous chapter.
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Building a list of endpoints
|
||||||
|
|
||||||
|
```Go
|
||||||
|
epnts, err := measurex.AllHTTPEndpointsForURL(parsedURL, http.Header{}, dns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
This is also code we have seen in previous chapters. The only
|
||||||
|
difference is that we supply empty headers since we're not going
|
||||||
|
to actually use the headers inside the endpoints.
|
||||||
|
|
||||||
|
### 3 and 4. Measure each endpoint
|
||||||
|
|
||||||
|
We will loop through the endpoints in the previous point
|
||||||
|
and issue the correct TCP or TLS primitive depending on
|
||||||
|
whether the input URL is HTTP or HTTPS.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
for _, epnt := range epnts {
|
||||||
|
switch parsedURL.Scheme {
|
||||||
|
case "http":
|
||||||
|
tcp := mx.TCPConnect(ctx, epnt.Address)
|
||||||
|
m.TCPConnect = append(m.TCPConnect, tcp.Connect...)
|
||||||
|
case "https":
|
||||||
|
config := &tls.Config{
|
||||||
|
ServerName: parsedURL.Hostname(),
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
RootCAs: netxlite.NewDefaultCertPool(),
|
||||||
|
}
|
||||||
|
tls := mx.TLSConnectAndHandshake(ctx, epnt.Address, config)
|
||||||
|
m.TCPConnect = append(m.TCPConnect, tls.Connect...)
|
||||||
|
m.TLSHandshakes = append(m.TLSHandshakes, tls.TLSHandshake...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point we've addressed points 1-4. So let's
|
||||||
|
now focus on the last point:
|
||||||
|
|
||||||
|
### 5. HTTP measurement
|
||||||
|
|
||||||
|
We need to manually build a `MeasurementDB`. This is a
|
||||||
|
"database" where networking code will store events.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
db := &measurex.MeasurementDB{}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Following the hint from the previous chapter we use the
|
||||||
|
`NewTracingHTTPTransportWithDefaultSettings` factory
|
||||||
|
to create an `http.Transport`-like object that will trace
|
||||||
|
HTTP round trip events writing them into `db`.
|
||||||
|
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
txp := measurex.NewTracingHTTPTransportWithDefaultSettings(mx.Begin, mx.Logger, db)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
We now build an `http.Client` using the transport
|
||||||
|
we've just created and a cookie jar (which we
|
||||||
|
use because otherwise some redirects will lead
|
||||||
|
to a redirect loop, as mentioned in previous chapters).
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
clnt := &http.Client{
|
||||||
|
Transport: txp,
|
||||||
|
Jar: measurex.NewCookieJar(),
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we use a method of the measurer that allows us to
|
||||||
|
perform an HTTP GET with an existing HTTP client
|
||||||
|
and a URL. This method will set a timeout and perform
|
||||||
|
the round trip. Reading a snapshot of the response
|
||||||
|
body is not implemented by this function but rather
|
||||||
|
is a property of the "tracing" HTTP transport we
|
||||||
|
created above (this type of transport is the one we
|
||||||
|
have been internally using in all the examples
|
||||||
|
presented so far.)
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
resp, _ := mx.HTTPClientGET(ctx, clnt, parsedURL)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
To be tidy, we also close the response body in case
|
||||||
|
we have a response. We don't really need to read
|
||||||
|
the body here. As mentioned previously, we're already
|
||||||
|
using an HTTP transport reading a body snapshot.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close() // tidy
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we append the round trips we performed into
|
||||||
|
the right field and return the measurement.
|
||||||
|
|
||||||
|
To this end, we're using the `db.AsMeasurement` method that
|
||||||
|
takes the current set of events into `db` and assembles
|
||||||
|
them into the `Measurement` struct we've been using in all
|
||||||
|
the chapters we have seen so far.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
m.Requests = append(m.Requests, db.AsMeasurement().HTTPRoundTrip...)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The rest of the program is pretty straightforward.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://www.google.com/", "URL to fetch")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
m, err := webConnectivity(ctx, *URL)
|
||||||
|
runtimex.PanicOnError(err, "invalid arguments to webConnectivity (wrong URL?)")
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the example program
|
||||||
|
|
||||||
|
Let us perform a vanilla run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run -race ./internal/tutorial/measurex/chapter14
|
||||||
|
```
|
||||||
|
|
||||||
|
Take a look at the JSON.
|
||||||
|
|
||||||
|
Now try running the program with `http://gmail.com` as
|
||||||
|
input. Take note of the redirect chain. See how the
|
||||||
|
domain changes during the redirect. Take note of the
|
||||||
|
fact that we are not measuring any TLS handshake. See
|
||||||
|
how we're not trying QUIC endpoints. These are, in
|
||||||
|
fact, some of the limitations of Web Connectivity that
|
||||||
|
we were trying to address when we wrote `measurex`.
|
||||||
|
|
||||||
|
Also, build the miniooni research client:
|
||||||
|
|
||||||
|
```
|
||||||
|
go build -v ./internal/cmd/miniooni
|
||||||
|
```
|
||||||
|
|
||||||
|
Run Web Connectivity with:
|
||||||
|
|
||||||
|
```
|
||||||
|
./miniooni -ni http://gmail.com web_connectivity
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes the report in a file named `report.jsonl`.
|
||||||
|
|
||||||
|
Check the content of the file and match it with the
|
||||||
|
output of this chapter. Are there other notable
|
||||||
|
differences between the two outputs?
|
||||||
|
|
||||||
|
### Bonus question
|
||||||
|
|
||||||
|
The solution we presented is true to the original
|
||||||
|
spirit of Web Connectivity, where we first perform
|
||||||
|
separate DNS, TCP/TLS steps, and then we also
|
||||||
|
perform a separate HTTP step. Is there in `measurex`
|
||||||
|
an API allowing you to invert the order of the
|
||||||
|
operations, that is:
|
||||||
|
|
||||||
|
1. build a full-fledged HTTP client where we can
|
||||||
|
trace _any_ operation;
|
||||||
|
|
||||||
|
2. use such client to measure the URL;
|
||||||
|
|
||||||
|
3. figure out what TCP endpoints we did not
|
||||||
|
test for TCP/TLS during this process and run
|
||||||
|
TCP/TLS testing only for them?
|
||||||
|
|
||||||
|
If such an API exist, can you write a simple
|
||||||
|
main.go client that implements points 1-3 above?
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We have presented the solution to the exercise
|
||||||
|
proposed in the previous chapter, i.e., how
|
||||||
|
to rewrite Web Connectivity using `measurex` API.
|
||||||
|
|
||||||
|
You have now been exposed to some complexity and
|
||||||
|
APIs to perform OONI measurements. So you should now
|
||||||
|
be read to help us write new and maitain existing
|
||||||
|
network experiments.
|
||||||
|
|
||||||
|
If you have further questions, please [contact us](
|
||||||
|
https://ooni.org/about/).
|
||||||
|
|
320
internal/tutorial/measurex/chapter14/main.go
Normal file
320
internal/tutorial/measurex/chapter14/main.go
Normal file
|
@ -0,0 +1,320 @@
|
||||||
|
// -=-=- StartHere -=-=-
|
||||||
|
//
|
||||||
|
// # Chapter XIV: A possible rewrite of Web Connectivity
|
||||||
|
//
|
||||||
|
// In this chapter we try to solve the exercise laid out in
|
||||||
|
// the previous chapter, using `measurex` primitives.
|
||||||
|
//
|
||||||
|
// (This file is auto-generated. Do not edit it directly! To apply
|
||||||
|
// changes you need to modify `./internal/tutorial/measurex/chapter14/main.go`.)
|
||||||
|
//
|
||||||
|
// ## main.go
|
||||||
|
//
|
||||||
|
// The beginning of the file is always pretty much the same.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func print(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
runtimex.PanicOnError(err, "json.Marshal failed")
|
||||||
|
fmt.Printf("%s\n", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## measurement type
|
||||||
|
//
|
||||||
|
// We define a measurement type with the fields
|
||||||
|
// that a Web Connectivity measurement should have.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
type measurement struct {
|
||||||
|
Queries []*measurex.DNSLookupEvent `json:"queries"`
|
||||||
|
TCPConnect []*measurex.NetworkEvent `json:"tcp_connect"`
|
||||||
|
TLSHandshakes []*measurex.TLSHandshakeEvent `json:"tls_handshakes"`
|
||||||
|
Requests []*measurex.HTTPRoundTripEvent `json:"requests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## WebConnectivity implementation
|
||||||
|
//
|
||||||
|
// We define a function that takes in input a context and a URL to
|
||||||
|
// measure and returns a measurement or an error.
|
||||||
|
//
|
||||||
|
// We will only error out in case the input does not allow us to
|
||||||
|
// proceed (i.e., invalid input URL).
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
func webConnectivity(ctx context.Context, URL string) (*measurement, error) {
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We start by parsing the input URL. If we cannot parse it, of
|
||||||
|
// course this is an hard error and we cannot continue.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
parsedURL, err := url.Parse(URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We create an empty measurement and a measurer with
|
||||||
|
// default settings like we did in the previous chapters.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
m := &measurement{}
|
||||||
|
mx := measurex.NewMeasurerWithDefaultSettings()
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Now it's time to start measuring. We will address all
|
||||||
|
// the points laid out in the previous chapter.
|
||||||
|
//
|
||||||
|
// ### 1. Enumerating IP addrs
|
||||||
|
//
|
||||||
|
// Let us enumerate all the IP addresses for
|
||||||
|
// the input URL's domain using the system resolver.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
dns := mx.LookupHostSystem(ctx, parsedURL.Hostname())
|
||||||
|
m.Queries = append(m.Queries, dns.LookupHost...)
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is code we have already seen in previous chapter.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// ### 2. Building a list of endpoints
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
epnts, err := measurex.AllHTTPEndpointsForURL(parsedURL, http.Header{}, dns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is also code we have seen in previous chapters. The only
|
||||||
|
// difference is that we supply empty headers since we're not going
|
||||||
|
// to actually use the headers inside the endpoints.
|
||||||
|
//
|
||||||
|
// ### 3 and 4. Measure each endpoint
|
||||||
|
//
|
||||||
|
// We will loop through the endpoints in the previous point
|
||||||
|
// and issue the correct TCP or TLS primitive depending on
|
||||||
|
// whether the input URL is HTTP or HTTPS.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
for _, epnt := range epnts {
|
||||||
|
switch parsedURL.Scheme {
|
||||||
|
case "http":
|
||||||
|
tcp := mx.TCPConnect(ctx, epnt.Address)
|
||||||
|
m.TCPConnect = append(m.TCPConnect, tcp.Connect...)
|
||||||
|
case "https":
|
||||||
|
config := &tls.Config{
|
||||||
|
ServerName: parsedURL.Hostname(),
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
RootCAs: netxlite.NewDefaultCertPool(),
|
||||||
|
}
|
||||||
|
tls := mx.TLSConnectAndHandshake(ctx, epnt.Address, config)
|
||||||
|
m.TCPConnect = append(m.TCPConnect, tls.Connect...)
|
||||||
|
m.TLSHandshakes = append(m.TLSHandshakes, tls.TLSHandshake...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// At this point we've addressed points 1-4. So let's
|
||||||
|
// now focus on the last point:
|
||||||
|
//
|
||||||
|
// ### 5. HTTP measurement
|
||||||
|
//
|
||||||
|
// We need to manually build a `MeasurementDB`. This is a
|
||||||
|
// "database" where networking code will store events.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
db := &measurex.MeasurementDB{}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Following the hint from the previous chapter we use the
|
||||||
|
// `NewTracingHTTPTransportWithDefaultSettings` factory
|
||||||
|
// to create an `http.Transport`-like object that will trace
|
||||||
|
// HTTP round trip events writing them into `db`.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
txp := measurex.NewTracingHTTPTransportWithDefaultSettings(mx.Begin, mx.Logger, db)
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We now build an `http.Client` using the transport
|
||||||
|
// we've just created and a cookie jar (which we
|
||||||
|
// use because otherwise some redirects will lead
|
||||||
|
// to a redirect loop, as mentioned in previous chapters).
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
clnt := &http.Client{
|
||||||
|
Transport: txp,
|
||||||
|
Jar: measurex.NewCookieJar(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Now we use a method of the measurer that allows us to
|
||||||
|
// perform an HTTP GET with an existing HTTP client
|
||||||
|
// and a URL. This method will set a timeout and perform
|
||||||
|
// the round trip. Reading a snapshot of the response
|
||||||
|
// body is not implemented by this function but rather
|
||||||
|
// is a property of the "tracing" HTTP transport we
|
||||||
|
// created above (this type of transport is the one we
|
||||||
|
// have been internally using in all the examples
|
||||||
|
// presented so far.)
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
resp, _ := mx.HTTPClientGET(ctx, clnt, parsedURL)
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// To be tidy, we also close the response body in case
|
||||||
|
// we have a response. We don't really need to read
|
||||||
|
// the body here. As mentioned previously, we're already
|
||||||
|
// using an HTTP transport reading a body snapshot.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close() // tidy
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Finally, we append the round trips we performed into
|
||||||
|
// the right field and return the measurement.
|
||||||
|
//
|
||||||
|
// To this end, we're using the `db.AsMeasurement` method that
|
||||||
|
// takes the current set of events into `db` and assembles
|
||||||
|
// them into the `Measurement` struct we've been using in all
|
||||||
|
// the chapters we have seen so far.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
m.Requests = append(m.Requests, db.AsMeasurement().HTTPRoundTrip...)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The rest of the program is pretty straightforward.
|
||||||
|
//
|
||||||
|
// ```Go
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
URL := flag.String("url", "https://www.google.com/", "URL to fetch")
|
||||||
|
timeout := flag.Duration("timeout", 60*time.Second, "timeout to use")
|
||||||
|
flag.Parse()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
m, err := webConnectivity(ctx, *URL)
|
||||||
|
runtimex.PanicOnError(err, "invalid arguments to webConnectivity (wrong URL?)")
|
||||||
|
print(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ## Running the example program
|
||||||
|
//
|
||||||
|
// Let us perform a vanilla run first:
|
||||||
|
//
|
||||||
|
// ```bash
|
||||||
|
// go run -race ./internal/tutorial/measurex/chapter14
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Take a look at the JSON.
|
||||||
|
//
|
||||||
|
// Now try running the program with `http://gmail.com` as
|
||||||
|
// input. Take note of the redirect chain. See how the
|
||||||
|
// domain changes during the redirect. Take note of the
|
||||||
|
// fact that we are not measuring any TLS handshake. See
|
||||||
|
// how we're not trying QUIC endpoints. These are, in
|
||||||
|
// fact, some of the limitations of Web Connectivity that
|
||||||
|
// we were trying to address when we wrote `measurex`.
|
||||||
|
//
|
||||||
|
// Also, build the miniooni research client:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// go build -v ./internal/cmd/miniooni
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Run Web Connectivity with:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// ./miniooni -ni http://gmail.com web_connectivity
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This writes the report in a file named `report.jsonl`.
|
||||||
|
//
|
||||||
|
// Check the content of the file and match it with the
|
||||||
|
// output of this chapter. Are there other notable
|
||||||
|
// differences between the two outputs?
|
||||||
|
//
|
||||||
|
// ### Bonus question
|
||||||
|
//
|
||||||
|
// The solution we presented is true to the original
|
||||||
|
// spirit of Web Connectivity, where we first perform
|
||||||
|
// separate DNS, TCP/TLS steps, and then we also
|
||||||
|
// perform a separate HTTP step. Is there in `measurex`
|
||||||
|
// an API allowing you to invert the order of the
|
||||||
|
// operations, that is:
|
||||||
|
//
|
||||||
|
// 1. build a full-fledged HTTP client where we can
|
||||||
|
// trace _any_ operation;
|
||||||
|
//
|
||||||
|
// 2. use such client to measure the URL;
|
||||||
|
//
|
||||||
|
// 3. figure out what TCP endpoints we did not
|
||||||
|
// test for TCP/TLS during this process and run
|
||||||
|
// TCP/TLS testing only for them?
|
||||||
|
//
|
||||||
|
// If such an API exist, can you write a simple
|
||||||
|
// main.go client that implements points 1-3 above?
|
||||||
|
//
|
||||||
|
// ## Conclusion
|
||||||
|
//
|
||||||
|
// We have presented the solution to the exercise
|
||||||
|
// proposed in the previous chapter, i.e., how
|
||||||
|
// to rewrite Web Connectivity using `measurex` API.
|
||||||
|
//
|
||||||
|
// You have now been exposed to some complexity and
|
||||||
|
// APIs to perform OONI measurements. So you should now
|
||||||
|
// be read to help us write new and maitain existing
|
||||||
|
// network experiments.
|
||||||
|
//
|
||||||
|
// If you have further questions, please [contact us](
|
||||||
|
// https://ooni.org/about/).
|
||||||
|
//
|
||||||
|
// -=-=- StopHere -=-=-
|
Loading…
Reference in New Issue
Block a user