kelmenhorst 1874f7a7c2
enable utls for websteps (#442)
This diff enables `websteps` to use uTLS for TLS parroting. It integrates the `oohttp.StdlibTransport` wrapper which uses the `ooni/oohttp` fork. `oohttp` supports TLS-like connections like `utls.Conn`.
As a prototype, the testhelper and `websteps` code now uses the `utls.HelloChrome_Auto` fingerprint, i.e. the simulated TLS fingerprint of the Google Chrome browser.

It is a further contribution for my GSoC project.

Reference issue: https://github.com/ooni/probe/issues/1733
2021-08-18 16:10:27 +02:00

470 lines
12 KiB
Go

package websteps
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"net/url"
"strings"
"testing"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/errorsx"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)
var generator = &DefaultGenerator{resolver: newResolver()}
type fakeTransport struct {
err error
}
func (txp fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return nil, txp.err
}
func (txp fakeTransport) CloseIdleConnections() {}
type fakeQUICDialer struct {
err error
}
func (d fakeQUICDialer) DialContext(ctx context.Context, network, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
return nil, d.err
}
type fakeDialer struct {
err error
}
func (d fakeDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return nil, d.err
}
func TestGenerateDNSFailure(t *testing.T) {
u, err := url.Parse("https://www.google.google")
runtimex.PanicOnError(err, "url.Parse failed")
rts := []*RoundTrip{
{
Proto: "https",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
},
}
urlMeasurements, err := generator.Generate(context.Background(), rts, []string{})
if err != nil {
t.Fatal("unexpected error")
}
if len(urlMeasurements) != 1 {
t.Fatal("unexpected urlMeasurements length")
}
if urlMeasurements[0].DNS == nil {
t.Fatal("DNS should not be nil")
}
if urlMeasurements[0].DNS.Failure == nil || *urlMeasurements[0].DNS.Failure != errorsx.FailureDNSNXDOMAINError {
t.Fatal("unexpected DNS failure type")
}
}
func TestGenerate(t *testing.T) {
u, err := url.Parse("http://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed for clearly good URL")
u2, err := url.Parse("https://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed for clearly good URL")
rts := []*RoundTrip{
{
Proto: "http",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
},
{
Proto: "https",
Request: &http.Request{
URL: u2,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
},
{
Proto: "h3",
Request: &http.Request{
URL: u2,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
},
}
urlMeasurements, err := generator.Generate(context.Background(), rts, []string{})
if err != nil {
t.Fatal("unexpected err")
}
if urlMeasurements == nil {
t.Fatal("unexpected nil urlMeasurement")
}
if len(urlMeasurements) < 3 {
t.Fatal("unexpected number of urlMeasurements", len(urlMeasurements))
}
}
func TestGenerateUnexpectedProtocol(t *testing.T) {
u, err := url.Parse("https://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed")
rts := []*RoundTrip{
{
Proto: "h3-27",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
},
}
urlMeasurements, err := generator.Generate(context.Background(), rts, []string{})
if err != nil {
t.Fatal("unexpected err")
}
if urlMeasurements == nil {
t.Fatal("unexpected nil urlMeasurement")
}
if len(urlMeasurements) != 1 {
t.Fatal("unexpected number of urlMeasurements")
}
measurement := urlMeasurements[0]
if measurement.URL != u.String() {
t.Fatal("unexpected URL")
}
if measurement.DNS == nil {
t.Fatal("DNS should not be nil")
}
if measurement.RoundTrip == nil {
t.Fatal("RoundTrip should not be nil")
}
if measurement.Endpoints != nil {
t.Fatal("Endpoints should be nil")
}
}
func TestGenerateURLWithClientResolutions(t *testing.T) {
u, err := url.Parse("https://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed")
rt := &RoundTrip{
Proto: "h3",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
}
clientResolution := "142.250.186.36"
urlMeasurement := generator.GenerateURL(context.Background(), rt, []string{clientResolution})
if err != nil {
t.Fatal("unexpected err")
}
if urlMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement")
}
if urlMeasurement.DNS == nil {
t.Fatal("DNS should not be nil")
}
if len(urlMeasurement.Endpoints) < 2 {
t.Fatal("unexpected number of endpoints")
}
clientAddrsFound := false
for _, e := range urlMeasurement.Endpoints {
if e.Endpoint == clientResolution+":443" {
clientAddrsFound = true
}
}
if !clientAddrsFound {
t.Fatal("did not use provided client resolution")
}
}
func TestGenerateHTTP(t *testing.T) {
u, err := url.Parse("http://example.com")
runtimex.PanicOnError(err, "url.Parse failed")
rt := &RoundTrip{
Proto: "http",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
}
endpointMeasurement := generator.GenerateHTTPEndpoint(context.Background(), rt, "93.184.216.34:80")
if err != nil {
t.Fatal("unexpected err")
}
if endpointMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement")
}
if endpointMeasurement.TCPConnect == nil {
t.Fatal("TCPConnectMeasurement should not be nil")
}
if endpointMeasurement.HTTPRoundTrip == nil {
t.Fatal("HTTPRoundTripMeasurement should not be nil")
}
}
func TestGenerateHTTPS(t *testing.T) {
u, err := url.Parse("https://example.com")
runtimex.PanicOnError(err, "url.Parse failed")
rt := &RoundTrip{
Proto: "https",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
}
endpointMeasurement := generator.GenerateHTTPSEndpoint(context.Background(), rt, "93.184.216.34:443")
if err != nil {
t.Fatal("unexpected err")
}
if endpointMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement")
}
if endpointMeasurement.TCPConnect == nil {
t.Fatal("TCPConnectMeasurement should not be nil")
}
if endpointMeasurement.TLSHandshake == nil {
t.Fatal("TLSHandshakeMeasurement should not be nil")
}
if endpointMeasurement.TLSHandshake.Failure != nil {
t.Fatal("unexpected failure at TLSHandshakeMeasurement")
}
if endpointMeasurement.HTTPRoundTrip == nil {
t.Fatal("HTTPRoundTripMeasurement should not be nil")
}
}
func TestGenerateHTTPSTLSFailure(t *testing.T) {
u, err := url.Parse("https://wrong.host.badssl.com/")
runtimex.PanicOnError(err, "url.Parse failed")
rt := &RoundTrip{
Proto: "https",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
}
endpointMeasurement := generator.GenerateHTTPSEndpoint(context.Background(), rt, "104.154.89.105:443")
if err != nil {
t.Fatal("unexpected err")
}
if endpointMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement")
}
if endpointMeasurement.TCPConnect == nil {
t.Fatal("TCPConnectMeasurement should not be nil")
}
if endpointMeasurement.TLSHandshake == nil {
t.Fatal("TLSHandshakeMeasurement should not be nil")
}
if endpointMeasurement.TLSHandshake.Failure == nil {
t.Fatal("expected failure at TLSHandshakeMeasurement")
}
if endpointMeasurement.HTTPRoundTrip != nil {
t.Fatal("HTTPRoundTripMeasurement should be nil")
}
}
func TestGenerateH3(t *testing.T) {
u, err := url.Parse("https://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed")
rt := &RoundTrip{
Proto: "h3",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
}
endpointMeasurement := generator.GenerateH3Endpoint(context.Background(), rt, "173.194.76.103:443")
if err != nil {
t.Fatal("unexpected err")
}
if endpointMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement")
}
if endpointMeasurement.QUICHandshake == nil {
t.Fatal("TCPConnectMeasurement should not be nil")
}
if endpointMeasurement.HTTPRoundTrip == nil {
t.Fatal("HTTPRoundTripMeasurement should not be nil")
}
}
func TestGenerateTCPDoFails(t *testing.T) {
expected := errors.New("expected")
generator := &DefaultGenerator{
dialer: fakeDialer{err: expected},
resolver: newResolver(),
}
u, err := url.Parse("https://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed")
rt := &RoundTrip{
Proto: "https",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
}
endpointMeasurement := generator.GenerateHTTPSEndpoint(context.Background(), rt, "173.194.76.103:443")
if err != nil {
t.Fatal("unexpected err")
}
if endpointMeasurement.TCPConnect == nil {
t.Fatal("QUIC handshake should not be nil")
}
if endpointMeasurement.TCPConnect.Failure == nil {
t.Fatal("expected an error here")
}
if *endpointMeasurement.TCPConnect.Failure != *newfailure(expected) {
t.Fatal("unexpected error type")
}
}
func TestGenerateQUICDoFails(t *testing.T) {
expected := errors.New("expected")
generator := &DefaultGenerator{
quicDialer: fakeQUICDialer{err: expected},
resolver: newResolver(),
}
u, err := url.Parse("https://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed")
rt := &RoundTrip{
Proto: "h3",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
}
endpointMeasurement := generator.GenerateH3Endpoint(context.Background(), rt, "173.194.76.103:443")
if err != nil {
t.Fatal("unexpected err")
}
if endpointMeasurement.QUICHandshake == nil {
t.Fatal("QUIC handshake should not be nil")
}
if endpointMeasurement.QUICHandshake.Failure == nil {
t.Fatal("expected an error here")
}
if *endpointMeasurement.QUICHandshake.Failure != *newfailure(expected) {
t.Fatal("unexpected error type")
}
}
func TestGenerateHTTPDoFails(t *testing.T) {
expected := errors.New("expected")
generator := &DefaultGenerator{
transport: fakeTransport{err: expected},
resolver: newResolver(),
}
u, err := url.Parse("http://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed for clearly good URL")
u2, err := url.Parse("https://www.google.com")
runtimex.PanicOnError(err, "url.Parse failed for clearly good URL")
rts := []*RoundTrip{
{
Proto: "http",
Request: &http.Request{
URL: u,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
},
{
Proto: "https",
Request: &http.Request{
URL: u2,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
},
{
Proto: "h3",
Request: &http.Request{
URL: u2,
},
Response: &http.Response{
StatusCode: 200,
},
SortIndex: 0,
},
}
urlMeasurements, err := generator.Generate(context.Background(), rts, []string{})
if err != nil {
t.Fatal("unexpected err")
}
if len(urlMeasurements) != 3 {
t.Fatal("unexpected number of urlMeasurements")
}
for _, u := range urlMeasurements {
if u.DNS == nil {
t.Fatal("unexpected DNS failure")
}
if len(u.Endpoints) < 1 {
t.Fatal("unexpected number of endpoints", len(u.Endpoints))
}
// this can occur when the network is unreachable, but it is irrelevant for checking HTTP behavior
if u.Endpoints[0].TCPConnect != nil && u.Endpoints[0].TCPConnect.Failure != nil {
continue
}
if u.Endpoints[0].QUICHandshake != nil && u.Endpoints[0].QUICHandshake.Failure != nil {
continue
}
if u.Endpoints[0].HTTPRoundTrip == nil {
t.Fatal("roundtrip should not be nil", u.Endpoints[0].TCPConnect.Failure, "jaaaa")
}
if u.Endpoints[0].HTTPRoundTrip.Response == nil {
t.Fatal("roundtrip response should not be nil")
}
if u.Endpoints[0].HTTPRoundTrip.Response.Failure == nil {
t.Fatal("expected an HTTP error")
}
if !strings.HasSuffix(*u.Endpoints[0].HTTPRoundTrip.Response.Failure, expected.Error()) {
t.Fatal("unexpected failure type")
}
}
}