ooni-probe-cli/internal/tutorial/netxlite/chapter04
Simone Basso 9ef4d9df7d
doc: add tutorial on how to use netxlite (#519)
The main tutorial will be the one at https://github.com/ooni/probe-cli/pull/506, but
it's useful to also document the primitives used by measurex.

So, here's the companion tutorial, which explains how to use the
features in netxlite to perform measurements.

This work is part of https://github.com/ooni/ooni.org/issues/361.
2021-09-28 18:15:38 +02:00
..
main.go doc: add tutorial on how to use netxlite (#519) 2021-09-28 18:15:38 +02:00
README.md doc: add tutorial on how to use netxlite (#519) 2021-09-28 18:15:38 +02:00

Chapter I: Using QUIC

In this chapter we will write together a main.go file that uses netxlite to establish a new QUIC session with an UDP endpoint.

Conceptually, this program is very similar to the ones presented in chapters 2 and 3, except that here we use QUIC.

(This file is auto-generated from the corresponding source file, so make sure you don't edit it manually.)

The main.go file

We define main.go file using package main.

The beginning of the program is equal to the previous chapters, so there is not much to say about it.

package main

import (
	"context"
	"crypto/tls"
	"errors"
	"flag"
	"os"
	"time"

	"github.com/apex/log"
	"github.com/lucas-clemente/quic-go"
	"github.com/ooni/probe-cli/v3/internal/netxlite"
)

func main() {
	log.SetLevel(log.DebugLevel)
	address := flag.String("address", "8.8.4.4:443", "Remote endpoint address")
	sni := flag.String("sni", "dns.google", "SNI to use")
	timeout := flag.Duration("timeout", 60*time.Second, "Timeout")
	flag.Parse()
	ctx, cancel := context.WithTimeout(context.Background(), *timeout)
	defer cancel()

The main difference is that we set the ALPN correctly for QUIC/HTTP3 by using "h3" here.

	config := &tls.Config{
		ServerName: *sni,
		NextProtos: []string{"h3"},
		RootCAs:    netxlite.NewDefaultCertPool(),
	}

Also, where previously we called dialTLS now we call a function with a similar API called dialQUIC.

	sess, state, err := dialQUIC(ctx, *address, config)

The rest of the main function is pretty much the same.

	if err != nil {
		fatal(err)
	}
	log.Infof("Sess type          : %T", sess)
	log.Infof("Cipher suite       : %s", netxlite.TLSCipherSuiteString(state.CipherSuite))
	log.Infof("Negotiated protocol: %s", state.NegotiatedProtocol)
	log.Infof("TLS version        : %s", netxlite.TLSVersionString(state.Version))
	sess.CloseWithError(0, "")
}

The dialQUIC function is new. We need to create a QUIC listener and, using it, a QUICDialer. These two steps are separated so higher level code can wrap the QUICDialer and collect stats on the returned connections. Also, as previously, this dialer is not attached to a resolver, so it will fail if provided a domain name. The rationale for doing that is similar to before: we are focusing on step-by-step measurements where each operation is performed independently. (That is, we assume that before the code written in this main we have already resolved the domain name of interest using a resolver, which we will investigate in the next two chapters.)

func dialQUIC(ctx context.Context, address string,
	config *tls.Config) (quic.EarlySession, tls.ConnectionState, error) {
	ql := netxlite.NewQUICListener()
	d := netxlite.NewQUICDialerWithoutResolver(ql, log.Log)
	sess, err := d.DialContext(ctx, "udp", address, config, &quic.Config{})
	if err != nil {
		return nil, tls.ConnectionState{}, err
	}

The following line unwraps the connection state returned by QUIC code to be of the same type of the ConnectionState that we returned in the previous chapters.

	return sess, sess.ConnectionState().TLS.ConnectionState, nil
}

The rest of the program is equal to the previous chapters.

func fatal(err error) {
	var ew *netxlite.ErrWrapper
	if !errors.As(err, &ew) {
		log.Fatal("cannot get ErrWrapper")
	}
	log.Warnf("error string    : %s", err.Error())
	log.Warnf("OONI failure    : %s", ew.Failure)
	log.Warnf("failed operation: %s", ew.Operation)
	log.Warnf("underlying error: %+v", ew.WrappedErr)
	os.Exit(1)
}

Running the code

Vanilla run

You can now run this code as follows:

go run -race ./internal/tutorial/netxlite/chapter04

You will see debug logs describing what is happening along with timing info.

QUIC handshake timeout

go run -race ./internal/tutorial/netxlite/chapter04 -address 8.8.4.4:1

should cause a QUIC timeout error. Try lowering the timout adding, e.g., the -timeout 5s flag to the command line.

SNI mismatch

go run -race ./internal/tutorial/netxlite/chapter04 -sni example.com

should give you a TLS error mentioning that the certificate is invalid.

Conclusions

We have seen how to use netxlite to establish a QUIC session with a remote UDP endpoint speaking QUIC.