b9a844ecee
* feat: run ~always netxlite integration tests This diff ensures that we check on windows, linux, macos that our fundamental networking library (netxlite) works. We combine unit and integration tests. This work is part of https://github.com/ooni/probe/issues/1733, where I want to have more strong guarantees about the foundations. * fix(filtering/tls_test.go): make portable on Windows The trick here is to use the wrapped error so to normalize the different errors messages we see on Windows. * fix(netxlite/quic_test.go): make portable on windows Rather than using the zero port, use the `x` port which fails when the stdlib is parsing the address. The zero port seems to work on Windows while it does not on Unix. * fix(serialresolver_test.go): make error more timeout than before This seems enough to convince Go on Windows about this error being really a timeout timeouty timeouted thingie.
498 lines
12 KiB
Go
498 lines
12 KiB
Go
package netxlite_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/lucas-clemente/quic-go"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite/filtering"
|
|
utls "gitlab.com/yawning/utls.git"
|
|
)
|
|
|
|
// This set of integration tests ensures that we continue to
|
|
// be able to measure the conditions we care about
|
|
|
|
func TestMeasureWithSystemResolver(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skip test in short mode")
|
|
}
|
|
|
|
//
|
|
// Measurement conditions we care about:
|
|
//
|
|
// - success
|
|
//
|
|
// - nxdomain
|
|
//
|
|
// - timeout
|
|
//
|
|
|
|
t.Run("on success", func(t *testing.T) {
|
|
r := netxlite.NewResolverStdlib(log.Log)
|
|
defer r.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
addrs, err := r.LookupHost(ctx, "dns.google.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if addrs == nil {
|
|
t.Fatal("expected non-nil result here")
|
|
}
|
|
})
|
|
|
|
t.Run("for nxdomain", func(t *testing.T) {
|
|
r := netxlite.NewResolverStdlib(log.Log)
|
|
defer r.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
addrs, err := r.LookupHost(ctx, "antani.ooni.org")
|
|
if err == nil || err.Error() != netxlite.FailureDNSNXDOMAINError {
|
|
t.Fatal("not the error we expected", err)
|
|
}
|
|
if addrs != nil {
|
|
t.Fatal("expected nil result here")
|
|
}
|
|
})
|
|
|
|
t.Run("for timeout", func(t *testing.T) {
|
|
r := netxlite.NewResolverStdlib(log.Log)
|
|
defer r.CloseIdleConnections()
|
|
const timeout = time.Nanosecond
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
addrs, err := r.LookupHost(ctx, "ooni.org")
|
|
if err == nil || err.Error() != netxlite.FailureGenericTimeoutError {
|
|
t.Fatal("not the error we expected", err)
|
|
}
|
|
if addrs != nil {
|
|
t.Fatal("expected nil result here")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMeasureWithUDPResolver(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skip test in short mode")
|
|
}
|
|
|
|
//
|
|
// Measurement conditions we care about:
|
|
//
|
|
// - success
|
|
//
|
|
// - nxdomain
|
|
//
|
|
// - refused
|
|
//
|
|
// - timeout
|
|
//
|
|
|
|
t.Run("on success", func(t *testing.T) {
|
|
dlr := netxlite.NewDialerWithoutResolver(log.Log)
|
|
r := netxlite.NewResolverUDP(log.Log, dlr, "8.8.4.4:53")
|
|
defer r.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
addrs, err := r.LookupHost(ctx, "dns.google.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if addrs == nil {
|
|
t.Fatal("expected non-nil result here")
|
|
}
|
|
})
|
|
|
|
t.Run("for nxdomain", func(t *testing.T) {
|
|
proxy := &filtering.DNSProxy{
|
|
OnQuery: func(domain string) filtering.DNSAction {
|
|
return filtering.DNSActionNXDOMAIN
|
|
},
|
|
}
|
|
listener, err := proxy.Start("127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
dlr := netxlite.NewDialerWithoutResolver(log.Log)
|
|
r := netxlite.NewResolverUDP(log.Log, dlr, listener.LocalAddr().String())
|
|
defer r.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
addrs, err := r.LookupHost(ctx, "ooni.org")
|
|
if err == nil || err.Error() != netxlite.FailureDNSNXDOMAINError {
|
|
t.Fatal("not the error we expected", err)
|
|
}
|
|
if addrs != nil {
|
|
t.Fatal("expected nil result here")
|
|
}
|
|
})
|
|
|
|
t.Run("for refused", func(t *testing.T) {
|
|
proxy := &filtering.DNSProxy{
|
|
OnQuery: func(domain string) filtering.DNSAction {
|
|
return filtering.DNSActionRefused
|
|
},
|
|
}
|
|
listener, err := proxy.Start("127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
dlr := netxlite.NewDialerWithoutResolver(log.Log)
|
|
r := netxlite.NewResolverUDP(log.Log, dlr, listener.LocalAddr().String())
|
|
defer r.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
addrs, err := r.LookupHost(ctx, "ooni.org")
|
|
if err == nil || err.Error() != netxlite.FailureDNSRefusedError {
|
|
t.Fatal("not the error we expected", err)
|
|
}
|
|
if addrs != nil {
|
|
t.Fatal("expected nil result here")
|
|
}
|
|
})
|
|
|
|
t.Run("for timeout", func(t *testing.T) {
|
|
proxy := &filtering.DNSProxy{
|
|
OnQuery: func(domain string) filtering.DNSAction {
|
|
return filtering.DNSActionTimeout
|
|
},
|
|
}
|
|
listener, err := proxy.Start("127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
dlr := netxlite.NewDialerWithoutResolver(log.Log)
|
|
r := netxlite.NewResolverUDP(log.Log, dlr, listener.LocalAddr().String())
|
|
defer r.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
addrs, err := r.LookupHost(ctx, "ooni.org")
|
|
if err == nil || err.Error() != netxlite.FailureGenericTimeoutError {
|
|
t.Fatal("not the error we expected", err)
|
|
}
|
|
if addrs != nil {
|
|
t.Fatal("expected nil result here")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMeasureWithDialer(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skip test in short mode")
|
|
}
|
|
|
|
//
|
|
// Measurement conditions we care about:
|
|
//
|
|
// - success
|
|
//
|
|
// - connection refused
|
|
//
|
|
// - timeout
|
|
//
|
|
|
|
t.Run("on success", func(t *testing.T) {
|
|
d := netxlite.NewDialerWithoutResolver(log.Log)
|
|
defer d.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
conn, err := d.DialContext(ctx, "tcp", "8.8.4.4:443")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if conn == nil {
|
|
t.Fatal("expected non-nil conn here")
|
|
}
|
|
conn.Close()
|
|
})
|
|
|
|
t.Run("on connection refused", func(t *testing.T) {
|
|
d := netxlite.NewDialerWithoutResolver(log.Log)
|
|
defer d.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
// Here we assume that no-one is listening on 127.0.0.1:1
|
|
conn, err := d.DialContext(ctx, "tcp", "127.0.0.1:1")
|
|
if err == nil || err.Error() != netxlite.FailureConnectionRefused {
|
|
t.Fatal("not the error we expected", err)
|
|
}
|
|
if conn != nil {
|
|
t.Fatal("expected nil conn here")
|
|
}
|
|
})
|
|
|
|
t.Run("on timeout", func(t *testing.T) {
|
|
d := netxlite.NewDialerWithoutResolver(log.Log)
|
|
defer d.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
// Here we assume 8.8.4.4:1 is filtered
|
|
conn, err := d.DialContext(ctx, "tcp", "8.8.4.4:1")
|
|
if err == nil || err.Error() != netxlite.FailureGenericTimeoutError {
|
|
t.Fatal("not the error we expected", err)
|
|
}
|
|
if conn != nil {
|
|
t.Fatal("expected nil conn here")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMeasureWithTLSHandshaker(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skip test in short mode")
|
|
}
|
|
|
|
//
|
|
// Measurement conditions we care about:
|
|
//
|
|
// - success
|
|
//
|
|
// - connection reset
|
|
//
|
|
// - timeout
|
|
//
|
|
|
|
dial := func(ctx context.Context, address string) (net.Conn, error) {
|
|
d := netxlite.NewDialerWithoutResolver(log.Log)
|
|
return d.DialContext(ctx, "tcp", address)
|
|
}
|
|
|
|
successFlow := func(th netxlite.TLSHandshaker) error {
|
|
ctx := context.Background()
|
|
conn, err := dial(ctx, "8.8.4.4:443")
|
|
if err != nil {
|
|
return fmt.Errorf("dial failed: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
config := &tls.Config{
|
|
ServerName: "dns.google",
|
|
NextProtos: []string{"h2", "http/1.1"},
|
|
RootCAs: netxlite.NewDefaultCertPool(),
|
|
}
|
|
tconn, _, err := th.Handshake(ctx, conn, config)
|
|
if err != nil {
|
|
return fmt.Errorf("tls handshake failed: %w", err)
|
|
}
|
|
tconn.Close()
|
|
return nil
|
|
}
|
|
|
|
connectionResetFlow := func(th netxlite.TLSHandshaker) error {
|
|
tlsProxy := &filtering.TLSProxy{
|
|
OnIncomingSNI: func(sni string) filtering.TLSAction {
|
|
return filtering.TLSActionReset
|
|
},
|
|
}
|
|
listener, err := tlsProxy.Start("127.0.0.1:0")
|
|
if err != nil {
|
|
return fmt.Errorf("cannot start proxy: %w", err)
|
|
}
|
|
defer listener.Close()
|
|
ctx := context.Background()
|
|
conn, err := dial(ctx, listener.Addr().String())
|
|
if err != nil {
|
|
return fmt.Errorf("dial failed: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
config := &tls.Config{
|
|
ServerName: "dns.google",
|
|
NextProtos: []string{"h2", "http/1.1"},
|
|
RootCAs: netxlite.NewDefaultCertPool(),
|
|
}
|
|
tconn, _, err := th.Handshake(ctx, conn, config)
|
|
if err == nil {
|
|
return fmt.Errorf("tls handshake succeded unexpectedly")
|
|
}
|
|
if err.Error() != netxlite.FailureConnectionReset {
|
|
return fmt.Errorf("not the error we expected: %w", err)
|
|
}
|
|
if tconn != nil {
|
|
return fmt.Errorf("expected nil tconn here")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
timeoutFlow := func(th netxlite.TLSHandshaker) error {
|
|
tlsProxy := &filtering.TLSProxy{
|
|
OnIncomingSNI: func(sni string) filtering.TLSAction {
|
|
return filtering.TLSActionTimeout
|
|
},
|
|
}
|
|
listener, err := tlsProxy.Start("127.0.0.1:0")
|
|
if err != nil {
|
|
return fmt.Errorf("cannot start proxy: %w", err)
|
|
}
|
|
defer listener.Close()
|
|
ctx := context.Background()
|
|
conn, err := dial(ctx, listener.Addr().String())
|
|
if err != nil {
|
|
return fmt.Errorf("dial failed: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
config := &tls.Config{
|
|
ServerName: "dns.google",
|
|
NextProtos: []string{"h2", "http/1.1"},
|
|
RootCAs: netxlite.NewDefaultCertPool(),
|
|
}
|
|
tconn, _, err := th.Handshake(ctx, conn, config)
|
|
if err == nil {
|
|
return fmt.Errorf("tls handshake succeded unexpectedly")
|
|
}
|
|
if err.Error() != netxlite.FailureGenericTimeoutError {
|
|
return fmt.Errorf("not the error we expected: %w", err)
|
|
}
|
|
if tconn != nil {
|
|
return fmt.Errorf("expected nil tconn here")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
t.Run("for stdlib handshaker", func(t *testing.T) {
|
|
t.Run("on success", func(t *testing.T) {
|
|
th := netxlite.NewTLSHandshakerStdlib(log.Log)
|
|
err := successFlow(th)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("on connection reset", func(t *testing.T) {
|
|
th := netxlite.NewTLSHandshakerStdlib(log.Log)
|
|
err := connectionResetFlow(th)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("on timeout", func(t *testing.T) {
|
|
th := netxlite.NewTLSHandshakerStdlib(log.Log)
|
|
err := timeoutFlow(th)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
})
|
|
|
|
t.Run("for utls handshaker", func(t *testing.T) {
|
|
t.Run("on success", func(t *testing.T) {
|
|
th := netxlite.NewTLSHandshakerUTLS(log.Log, &utls.HelloFirefox_55)
|
|
err := successFlow(th)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("on connection reset", func(t *testing.T) {
|
|
th := netxlite.NewTLSHandshakerUTLS(log.Log, &utls.HelloFirefox_55)
|
|
err := connectionResetFlow(th)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("on timeout", func(t *testing.T) {
|
|
th := netxlite.NewTLSHandshakerUTLS(log.Log, &utls.HelloFirefox_55)
|
|
err := timeoutFlow(th)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestMeasureWithQUICDialer(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skip test in short mode")
|
|
}
|
|
|
|
//
|
|
// Measurement conditions we care about:
|
|
//
|
|
// - success
|
|
//
|
|
// - timeout
|
|
//
|
|
|
|
t.Run("on success", func(t *testing.T) {
|
|
ql := netxlite.NewQUICListener()
|
|
d := netxlite.NewQUICDialerWithoutResolver(ql, log.Log)
|
|
defer d.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
config := &tls.Config{
|
|
ServerName: "dns.google",
|
|
NextProtos: []string{"h3"},
|
|
RootCAs: netxlite.NewDefaultCertPool(),
|
|
}
|
|
sess, err := d.DialContext(ctx, "udp", "8.8.4.4:443", config, &quic.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sess == nil {
|
|
t.Fatal("expected non-nil sess here")
|
|
}
|
|
sess.CloseWithError(0, "")
|
|
})
|
|
|
|
t.Run("on timeout", func(t *testing.T) {
|
|
ql := netxlite.NewQUICListener()
|
|
d := netxlite.NewQUICDialerWithoutResolver(ql, log.Log)
|
|
defer d.CloseIdleConnections()
|
|
ctx := context.Background()
|
|
config := &tls.Config{
|
|
ServerName: "dns.google",
|
|
NextProtos: []string{"h3"},
|
|
RootCAs: netxlite.NewDefaultCertPool(),
|
|
}
|
|
// Here we assume 8.8.4.4:1 is filtered
|
|
sess, err := d.DialContext(ctx, "udp", "8.8.4.4:1", config, &quic.Config{})
|
|
if err == nil || err.Error() != netxlite.FailureGenericTimeoutError {
|
|
t.Fatal("not the error we expected", err)
|
|
}
|
|
if sess != nil {
|
|
t.Fatal("expected nil sess here")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHTTPTransport(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skip test in short mode")
|
|
}
|
|
|
|
t.Run("works as intended", func(t *testing.T) {
|
|
d := netxlite.NewDialerWithResolver(log.Log, netxlite.NewResolverStdlib(log.Log))
|
|
td := netxlite.NewTLSDialer(d, netxlite.NewTLSHandshakerStdlib(log.Log))
|
|
txp := netxlite.NewHTTPTransport(log.Log, d, td)
|
|
client := &http.Client{Transport: txp}
|
|
resp, err := client.Get("https://www.google.com/robots.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
client.CloseIdleConnections()
|
|
})
|
|
}
|
|
|
|
func TestHTTP3Transport(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skip test in short mode")
|
|
}
|
|
|
|
t.Run("works as intended", func(t *testing.T) {
|
|
d := netxlite.NewQUICDialerWithResolver(
|
|
netxlite.NewQUICListener(),
|
|
log.Log,
|
|
netxlite.NewResolverStdlib(log.Log),
|
|
)
|
|
txp := netxlite.NewHTTP3Transport(log.Log, d, &tls.Config{})
|
|
client := &http.Client{Transport: txp}
|
|
resp, err := client.Get("https://www.google.com/robots.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
txp.CloseIdleConnections()
|
|
})
|
|
}
|