ooni-probe-cli/internal/measurexlite/quic_test.go
Simone Basso b78b9aca51
refactor(datafmt): use "udp" instead of "quic" (#946)
This diff changes the data format to prefer "udp" to "quic" everywhere we were previously using "quic".

Previously, the code inconsistently used "quic" for operations where we knew we were using "quic" and "udp" otherwise (e.g., for generic operations like ReadFrom).

While it would be more correct to say that a specific HTTP request used "quic" rather than "udp", using "udp" consistently allows one to see how distinct events such as ReadFrom and an handshake all refer to the same address, port, and protocol triple. Therefore, this change makes it easier to programmatically unpack a single measurement and create endpoint stats.

Before implementing this change, I discussed the problem with @hellais who mentioned that ooni/data is not currently using the "quic" string anywhere. I know that ooni/pipeline also doesn't rely on this string. The only users of this feature have been research-oriented experiments such as urlgetter, for which such a change would actually be acceptable.

See https://github.com/ooni/probe/issues/2238 and https://github.com/ooni/spec/pull/262.
2022-09-08 17:19:59 +02:00

309 lines
8.6 KiB
Go

package measurexlite
import (
"context"
"crypto/tls"
"errors"
"net"
"syscall"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/testingx"
)
func TestNewQUICDialerWithoutResolver(t *testing.T) {
t.Run("NewQUICDialerWithoutResolver creates a wrapped dialer", func(t *testing.T) {
underlying := &mocks.QUICDialer{}
zeroTime := time.Now()
trace := NewTrace(0, zeroTime)
trace.NewQUICDialerWithoutResolverFn = func(listener model.QUICListener, dl model.DebugLogger) model.QUICDialer {
return underlying
}
listener := &mocks.QUICListener{}
dialer := trace.NewQUICDialerWithoutResolver(listener, model.DiscardLogger)
dt := dialer.(*quicDialerTrace)
if dt.qd != underlying {
t.Fatal("invalid quic dialer")
}
if dt.tx != trace {
t.Fatal("invalid trace")
}
})
t.Run("DialContext calls the underlying dialer with context-based tracing", func(t *testing.T) {
expectedErr := errors.New("mocked err")
zeroTime := time.Now()
trace := NewTrace(0, zeroTime)
var hasCorrectTrace bool
underlying := &mocks.QUICDialer{
MockDialContext: func(ctx context.Context, address string, tlsConfig *tls.Config,
quicConfig *quic.Config) (quic.EarlyConnection, error) {
gotTrace := netxlite.ContextTraceOrDefault(ctx)
hasCorrectTrace = (gotTrace == trace)
return nil, expectedErr
},
}
trace.NewQUICDialerWithoutResolverFn = func(listener model.QUICListener, dl model.DebugLogger) model.QUICDialer {
return underlying
}
listener := &mocks.QUICListener{}
dialer := trace.NewQUICDialerWithoutResolver(listener, model.DiscardLogger)
ctx := context.Background()
conn, err := dialer.DialContext(ctx, "1.1.1.1:443", &tls.Config{}, &quic.Config{})
if !errors.Is(err, expectedErr) {
t.Fatal("unexpected err", err)
}
if conn != nil {
t.Fatal("expected nil conn")
}
if !hasCorrectTrace {
t.Fatal("does not have the correct trace")
}
})
t.Run("CloseIdleConnection is correctly forwarded", func(t *testing.T) {
var called bool
zeroTime := time.Now()
trace := NewTrace(0, zeroTime)
underlying := &mocks.QUICDialer{
MockCloseIdleConnections: func() {
called = true
},
}
trace.NewQUICDialerWithoutResolverFn = func(listener model.QUICListener, dl model.DebugLogger) model.QUICDialer {
return underlying
}
listener := &mocks.QUICListener{}
dialer := trace.NewQUICDialerWithoutResolver(listener, model.DiscardLogger)
dialer.CloseIdleConnections()
if !called {
t.Fatal("not called")
}
})
t.Run("DialContext saves into trace", func(t *testing.T) {
mockedErr := errors.New("mocked")
zeroTime := time.Now()
td := testingx.NewTimeDeterministic(zeroTime)
trace := NewTrace(0, zeroTime)
trace.TimeNowFn = td.Now // deterministic time tracking
pconn := &mocks.UDPLikeConn{
MockLocalAddr: func() net.Addr {
return &net.UDPAddr{
Port: 0,
}
},
MockRemoteAddr: func() net.Addr {
return &net.UDPAddr{
Port: 0,
}
},
MockSyscallConn: func() (syscall.RawConn, error) {
return nil, mockedErr
},
MockClose: func() error {
return nil
},
}
listener := &mocks.QUICListener{
MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
return pconn, nil
},
}
dialer := trace.NewQUICDialerWithoutResolver(listener, model.DiscardLogger)
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: "dns.cloudflare.com",
}
ctx := context.Background()
qconn, err := dialer.DialContext(ctx, "1.1.1.1:443", tlsConfig, &quic.Config{})
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if qconn != nil {
t.Fatal("expected nil qconn")
}
t.Run("QUICHandshake events", func(t *testing.T) {
events := trace.QUICHandshakes()
if len(events) != 1 {
t.Fatal("expected to see single QUICHandshake event")
}
expectedFailure := "unknown_failure: mocked"
expect := &model.ArchivalTLSOrQUICHandshakeResult{
Network: "udp",
Address: "1.1.1.1:443",
CipherSuite: "",
Failure: &expectedFailure,
NegotiatedProtocol: "",
NoTLSVerify: true,
PeerCertificates: []model.ArchivalMaybeBinaryData{},
ServerName: "dns.cloudflare.com",
T: time.Second.Seconds(),
Tags: []string{},
TLSVersion: "",
}
got := events[0]
if diff := cmp.Diff(expect, got); diff != "" {
t.Fatal(diff)
}
})
t.Run("Network events", func(t *testing.T) {
events := trace.NetworkEvents()
if len(events) != 2 {
t.Fatal("expected to see three Network events")
}
t.Run("quic_handshake_start", func(t *testing.T) {
expect := &model.ArchivalNetworkEvent{
Address: "",
Failure: nil,
NumBytes: 0,
Operation: "quic_handshake_start",
Proto: "",
T: 0,
Tags: []string{},
}
got := events[0]
if diff := cmp.Diff(expect, got); diff != "" {
t.Fatal(diff)
}
})
t.Run("quic_handshake_done", func(t *testing.T) {
expect := &model.ArchivalNetworkEvent{
Address: "",
Failure: nil,
NumBytes: 0,
Operation: "quic_handshake_done",
Proto: "",
T0: time.Second.Seconds(),
T: time.Second.Seconds(),
Tags: []string{},
}
got := events[1]
if diff := cmp.Diff(expect, got); diff != "" {
t.Fatal(diff)
}
})
})
})
t.Run("DialContext discards events when buffer is full", func(t *testing.T) {
mockedErr := errors.New("mocked")
zeroTime := time.Now()
trace := NewTrace(0, zeroTime)
trace.networkEvent = make(chan *model.ArchivalNetworkEvent) // no buffer
trace.quicHandshake = make(chan *model.ArchivalTLSOrQUICHandshakeResult) // no buffer
pconn := &mocks.UDPLikeConn{
MockLocalAddr: func() net.Addr {
return &net.UDPAddr{
Port: 0,
}
},
MockRemoteAddr: func() net.Addr {
return &net.UDPAddr{
Port: 0,
}
},
MockSyscallConn: func() (syscall.RawConn, error) {
return nil, mockedErr
},
MockClose: func() error {
return nil
},
}
listener := &mocks.QUICListener{
MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
return pconn, nil
},
}
dialer := trace.NewQUICDialerWithoutResolver(listener, model.DiscardLogger)
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: "dns.cloudflare.com",
}
ctx := context.Background()
qconn, err := dialer.DialContext(ctx, "1.1.1.1:443", tlsConfig, &quic.Config{})
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if qconn != nil {
t.Fatal("expected nil qconn")
}
t.Run("QUiCHandshake events", func(t *testing.T) {
events := trace.QUICHandshakes()
if len(events) != 0 {
t.Fatal("expected to see no QUICHandshake events")
}
})
t.Run("Network events", func(t *testing.T) {
events := trace.NetworkEvents()
if len(events) != 0 {
t.Fatal("expected to see no network events")
}
})
})
}
func TestFirstQUICHandshake(t *testing.T) {
t.Run("returns nil when buffer is empty", func(t *testing.T) {
zeroTime := time.Now()
trace := NewTrace(0, zeroTime)
got := trace.FirstQUICHandshakeOrNil()
if got != nil {
t.Fatal("expected nil event")
}
})
t.Run("return first non-nil QUICHandshake", func(t *testing.T) {
filler := func(tx *Trace, events []*model.ArchivalTLSOrQUICHandshakeResult) {
for _, ev := range events {
tx.quicHandshake <- ev
}
}
zeroTime := time.Now()
trace := NewTrace(0, zeroTime)
expect := []*model.ArchivalTLSOrQUICHandshakeResult{{
Network: "udp",
Address: "1.1.1.1:443",
CipherSuite: "",
Failure: nil,
NegotiatedProtocol: "",
NoTLSVerify: true,
PeerCertificates: []model.ArchivalMaybeBinaryData{},
ServerName: "dns.cloudflare.com",
T: time.Second.Seconds(),
Tags: []string{},
TLSVersion: "",
}, {
Network: "udp",
Address: "8.8.8.8:443",
CipherSuite: "",
Failure: nil,
NegotiatedProtocol: "",
NoTLSVerify: true,
PeerCertificates: []model.ArchivalMaybeBinaryData{},
ServerName: "dns.google.com",
T: time.Second.Seconds(),
Tags: []string{},
TLSVersion: "",
}}
filler(trace, expect)
got := trace.FirstQUICHandshakeOrNil()
if diff := cmp.Diff(got, expect[0]); diff != "" {
t.Fatal(diff)
}
})
}