feat: run ~always netxlite integration tests (#522)
* 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.
This commit is contained in:
parent
9967803c31
commit
b9a844ecee
14 changed files with 1577 additions and 97 deletions
|
|
@ -3,27 +3,42 @@ 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"
|
||||
)
|
||||
|
||||
func TestResolver(t *testing.T) {
|
||||
// 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")
|
||||
}
|
||||
|
||||
t.Run("works as intended", func(t *testing.T) {
|
||||
// TODO(bassosimone): this is actually an integration
|
||||
// test but how to test this case?
|
||||
//
|
||||
// Measurement conditions we care about:
|
||||
//
|
||||
// - success
|
||||
//
|
||||
// - nxdomain
|
||||
//
|
||||
// - timeout
|
||||
//
|
||||
|
||||
t.Run("on success", func(t *testing.T) {
|
||||
r := netxlite.NewResolverStdlib(log.Log)
|
||||
defer r.CloseIdleConnections()
|
||||
addrs, err := r.LookupHost(context.Background(), "dns.google.com")
|
||||
ctx := context.Background()
|
||||
addrs, err := r.LookupHost(ctx, "dns.google.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -31,6 +46,413 @@ func TestResolver(t *testing.T) {
|
|||
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) {
|
||||
|
|
@ -73,63 +495,3 @@ func TestHTTP3Transport(t *testing.T) {
|
|||
txp.CloseIdleConnections()
|
||||
})
|
||||
}
|
||||
|
||||
func TestUTLSHandshaker(t *testing.T) {
|
||||
t.Run("with chrome fingerprint", func(t *testing.T) {
|
||||
h := netxlite.NewTLSHandshakerUTLS(log.Log, &utls.HelloChrome_Auto)
|
||||
cfg := &tls.Config{ServerName: "google.com"}
|
||||
conn, err := net.Dial("tcp", "google.com:443")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
conn, _, err = h.Handshake(context.Background(), conn, cfg)
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
if conn == nil {
|
||||
t.Fatal("nil connection")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestQUICDialer(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
|
||||
t.Run("works as intended", func(t *testing.T) {
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: "dns.google",
|
||||
}
|
||||
d := netxlite.NewQUICDialerWithoutResolver(
|
||||
netxlite.NewQUICListener(), log.Log,
|
||||
)
|
||||
ctx := context.Background()
|
||||
sess, err := d.DialContext(
|
||||
ctx, "udp", "8.8.8.8:443", tlsConfig, &quic.Config{})
|
||||
if err != nil {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
<-sess.HandshakeComplete().Done()
|
||||
if err := sess.CloseWithError(0, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("can guess the SNI and ALPN when using a domain name for web", func(t *testing.T) {
|
||||
d := netxlite.NewQUICDialerWithResolver(
|
||||
netxlite.NewQUICListener(), log.Log,
|
||||
netxlite.NewResolverStdlib(log.Log),
|
||||
)
|
||||
ctx := context.Background()
|
||||
sess, err := d.DialContext(
|
||||
ctx, "udp", "dns.google:443", &tls.Config{}, &quic.Config{})
|
||||
if err != nil {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
<-sess.HandshakeComplete().Done()
|
||||
if err := sess.CloseWithError(0, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue