chapter14

command
v3.24.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 25, 2024 License: GPL-3.0 Imports: 10 Imported by: 0

README

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.

package main

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"flag"
	"fmt"
	"net/http"
	"net/url"
	"time"

	"github.com/ooni/probe-cli/v3/internal/legacy/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))
}

measurement type

We define a measurement type with the fields that a Web Connectivity measurement should have.


type measurement struct {
	Queries       []*measurex.ArchivalDNSLookupEvent        `json:"queries"`
	TCPConnect    []*measurex.ArchivalTCPConnect            `json:"tcp_connect"`
	TLSHandshakes []*measurex.ArchivalQUICTLSHandshakeEvent `json:"tls_handshakes"`
	Requests      []*measurex.ArchivalHTTPRoundTripEvent    `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).


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 a hard error and we cannot continue.

	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.

	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.

	dns := mx.LookupHostSystem(ctx, parsedURL.Hostname())
	m.Queries = append(
		m.Queries, measurex.NewArchivalDNSLookupEventList(dns.LookupHost)...)

This is code we have already seen in the previous chapters.

2. Building a list of endpoints
	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.

	for _, epnt := range epnts {
		switch parsedURL.Scheme {
		case "http":
			tcp := mx.TCPConnect(ctx, epnt.Address)
			m.TCPConnect = append(
				m.TCPConnect, measurex.NewArchivalTCPConnectList(tcp.Connect)...)
		case "https":
			config := &tls.Config{ // #nosec G402 - we need to use a large TLS versions range for measuring
				ServerName: parsedURL.Hostname(),
				NextProtos: []string{"h2", "http/1.1"},
				RootCAs:    nil, // use netxlite's default
			}
			tls := mx.TLSConnectAndHandshake(ctx, epnt.Address, config)
			m.TCPConnect = append(
				m.TCPConnect, measurex.NewArchivalTCPConnectList(tls.Connect)...)
			m.TLSHandshakes = append(m.TLSHandshakes,
				measurex.NewArchivalQUICTLSHandshakeEventList(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 the networking code will store events.


	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.


	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).


	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 using internally in all the examples presented so far.)


	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.


	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.


	m.Requests = append(m.Requests, measurex.NewArchivalHTTPRoundTripEventList(
		db.AsMeasurement().HTTPRoundTrip)...)
	return m, nil
}

The rest of the program is pretty straightforward.


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:

go run -race ./internal/tutorial/measurex/chapter14 | jq

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.

Documentation

Overview

-=-=- 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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL