From 3ba5626b953d12a0d3a8903ea741c4566b3f67f0 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Mon, 6 Sep 2021 20:56:14 +0200 Subject: [PATCH] feat(netxlite): add CloseIdleConnections to quic dialer (#469) Like before, do not touch the rest of the tree. Rather create compatibility types declared as legacy. We will soon be able to close idle connections for an HTTP3 transport using any kind of resolvers more easily. See https://github.com/ooni/probe/issues/1591 --- .../engine/experiment/websteps/factory.go | 2 +- internal/engine/netx/netx.go | 2 +- internal/netxlite/legacy.go | 39 ++++++++++++++++ internal/netxlite/legacy_test.go | 18 ++++++++ internal/netxlite/mocks/legacy.go | 22 +++++++++ internal/netxlite/mocks/legacy_test.go | 29 ++++++++++++ internal/netxlite/mocks/quic.go | 15 +++++-- internal/netxlite/mocks/quic_test.go | 17 ++++++- internal/netxlite/quic.go | 41 ++++++++++++----- internal/netxlite/quic_test.go | 45 +++++++++++++++++-- 10 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 internal/netxlite/mocks/legacy.go create mode 100644 internal/netxlite/mocks/legacy_test.go diff --git a/internal/engine/experiment/websteps/factory.go b/internal/engine/experiment/websteps/factory.go index 621d3c6..bf929df 100644 --- a/internal/engine/experiment/websteps/factory.go +++ b/internal/engine/experiment/websteps/factory.go @@ -54,7 +54,7 @@ func NewQUICDialerResolver(resolver netxlite.ResolverLegacy) netxlite.QUICContex dialer = &errorsx.ErrorWrapperQUICDialer{Dialer: dialer} dialer = &netxlite.QUICDialerResolver{ Resolver: netxlite.NewResolverLegacyAdapter(resolver), - Dialer: dialer, + Dialer: netxlite.NewQUICDialerFromContextDialerAdapter(dialer), } return dialer } diff --git a/internal/engine/netx/netx.go b/internal/engine/netx/netx.go index 4644353..946e92c 100644 --- a/internal/engine/netx/netx.go +++ b/internal/engine/netx/netx.go @@ -181,7 +181,7 @@ func NewQUICDialer(config Config) QUICDialer { } d = &netxlite.QUICDialerResolver{ Resolver: netxlite.NewResolverLegacyAdapter(config.FullResolver), - Dialer: d, + Dialer: netxlite.NewQUICDialerFromContextDialerAdapter(d), } return d } diff --git a/internal/netxlite/legacy.go b/internal/netxlite/legacy.go index d2ddb98..9a28cb0 100644 --- a/internal/netxlite/legacy.go +++ b/internal/netxlite/legacy.go @@ -2,7 +2,10 @@ package netxlite import ( "context" + "crypto/tls" "net" + + "github.com/lucas-clemente/quic-go" ) // These vars export internal names to legacy ooni/probe-cli code. @@ -129,3 +132,39 @@ func (d *DialerLegacyAdapter) CloseIdleConnections() { ra.CloseIdleConnections() } } + +// QUICContextDialer is a dialer for QUIC using Context. +// +// This is a LEGACY name. New code should use QUICDialer directly. +// +// Use NewQUICDialerFromContextDialerAdapter if you need to +// adapt an existing QUICContextDialer to a QUICDialer. +type QUICContextDialer interface { + // DialContext establishes a new QUIC session using the given + // network and address. The tlsConfig and the quicConfig arguments + // MUST NOT be nil. Returns either the session or an error. + DialContext(ctx context.Context, network, address string, + tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) +} + +// NewQUICDialerFromContextDialerAdapter creates a new +// QUICDialer from a QUICContextDialer. +func NewQUICDialerFromContextDialerAdapter(d QUICContextDialer) QUICDialer { + return &QUICContextDialerAdapter{d} +} + +// QUICContextDialerAdapter adapta a QUICContextDialer to be a QUICDialer. +type QUICContextDialerAdapter struct { + QUICContextDialer +} + +type quicContextDialerConnectionsCloser interface { + CloseIdleConnections() +} + +// CloseIdleConnections implements QUICDialer.CloseIdleConnections. +func (d *QUICContextDialerAdapter) CloseIdleConnections() { + if o, ok := d.QUICContextDialer.(quicContextDialerConnectionsCloser); ok { + o.CloseIdleConnections() + } +} diff --git a/internal/netxlite/legacy_test.go b/internal/netxlite/legacy_test.go index 5963f18..d994f40 100644 --- a/internal/netxlite/legacy_test.go +++ b/internal/netxlite/legacy_test.go @@ -60,3 +60,21 @@ func TestDialerLegacyAdapterDefaults(t *testing.T) { r := NewDialerLegacyAdapter(&net.Dialer{}) r.CloseIdleConnections() // does not crash } + +func TestQUICContextDialerAdapterWithCompatibleType(t *testing.T) { + var called bool + d := NewQUICDialerFromContextDialerAdapter(&mocks.QUICDialer{ + MockCloseIdleConnections: func() { + called = true + }, + }) + d.CloseIdleConnections() + if !called { + t.Fatal("not called") + } +} + +func TestQUICContextDialerAdapterDefaults(t *testing.T) { + d := NewQUICDialerFromContextDialerAdapter(&mocks.QUICContextDialer{}) + d.CloseIdleConnections() // does not crash +} diff --git a/internal/netxlite/mocks/legacy.go b/internal/netxlite/mocks/legacy.go new file mode 100644 index 0000000..2f6dceb --- /dev/null +++ b/internal/netxlite/mocks/legacy.go @@ -0,0 +1,22 @@ +package mocks + +import ( + "context" + "crypto/tls" + + "github.com/lucas-clemente/quic-go" +) + +// QUICContextDialer is a mockable netxlite.QUICContextDialer. +// +// DEPRECATED: please use QUICDialer. +type QUICContextDialer struct { + MockDialContext func(ctx context.Context, network, address string, + tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) +} + +// DialContext calls MockDialContext. +func (qcd *QUICContextDialer) DialContext(ctx context.Context, network, address string, + tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) { + return qcd.MockDialContext(ctx, network, address, tlsConfig, quicConfig) +} diff --git a/internal/netxlite/mocks/legacy_test.go b/internal/netxlite/mocks/legacy_test.go new file mode 100644 index 0000000..d4e739e --- /dev/null +++ b/internal/netxlite/mocks/legacy_test.go @@ -0,0 +1,29 @@ +package mocks + +import ( + "context" + "crypto/tls" + "errors" + "testing" + + "github.com/lucas-clemente/quic-go" +) + +func TestQUICContextDialerDialContext(t *testing.T) { + expected := errors.New("mocked error") + qcd := &QUICContextDialer{ + MockDialContext: func(ctx context.Context, network string, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) { + return nil, expected + }, + } + ctx := context.Background() + tlsConfig := &tls.Config{} + quicConfig := &quic.Config{} + sess, err := qcd.DialContext(ctx, "udp", "dns.google:443", tlsConfig, quicConfig) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected") + } + if sess != nil { + t.Fatal("expected nil session") + } +} diff --git a/internal/netxlite/mocks/quic.go b/internal/netxlite/mocks/quic.go index 2cf78e3..3adf446 100644 --- a/internal/netxlite/mocks/quic.go +++ b/internal/netxlite/mocks/quic.go @@ -21,18 +21,27 @@ func (ql *QUICListener) Listen(addr *net.UDPAddr) (quicx.UDPLikeConn, error) { return ql.MockListen(addr) } -// QUICContextDialer is a mockable netxlite.QUICContextDialer. -type QUICContextDialer struct { +// QUICDialer is a mockable netxlite.QUICDialer. +type QUICDialer struct { + // MockDialContext allows mocking DialContext. MockDialContext func(ctx context.Context, network, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) + + // MockCloseIdleConnections allows mocking CloseIdleConnections. + MockCloseIdleConnections func() } // DialContext calls MockDialContext. -func (qcd *QUICContextDialer) DialContext(ctx context.Context, network, address string, +func (qcd *QUICDialer) DialContext(ctx context.Context, network, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) { return qcd.MockDialContext(ctx, network, address, tlsConfig, quicConfig) } +// CloseIdleConnections calls MockCloseIdleConnections. +func (qcd *QUICDialer) CloseIdleConnections() { + qcd.MockCloseIdleConnections() +} + // QUICEarlySession is a mockable quic.EarlySession. type QUICEarlySession struct { MockAcceptStream func(context.Context) (quic.Stream, error) diff --git a/internal/netxlite/mocks/quic_test.go b/internal/netxlite/mocks/quic_test.go index 7da1cdf..614a5ad 100644 --- a/internal/netxlite/mocks/quic_test.go +++ b/internal/netxlite/mocks/quic_test.go @@ -31,9 +31,9 @@ func TestQUICListenerListen(t *testing.T) { } } -func TestQUICContextDialerDialContext(t *testing.T) { +func TestQUICDialerDialContext(t *testing.T) { expected := errors.New("mocked error") - qcd := &QUICContextDialer{ + qcd := &QUICDialer{ MockDialContext: func(ctx context.Context, network string, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) { return nil, expected }, @@ -50,6 +50,19 @@ func TestQUICContextDialerDialContext(t *testing.T) { } } +func TestQUICDialerCloseIdleConnections(t *testing.T) { + var called bool + qcd := &QUICDialer{ + MockCloseIdleConnections: func() { + called = true + }, + } + qcd.CloseIdleConnections() + if !called { + t.Fatal("not called") + } +} + func TestQUICEarlySessionAcceptStream(t *testing.T) { expected := errors.New("mocked error") sess := &QUICEarlySession{ diff --git a/internal/netxlite/quic.go b/internal/netxlite/quic.go index ccb3785..439a7f7 100644 --- a/internal/netxlite/quic.go +++ b/internal/netxlite/quic.go @@ -11,13 +11,16 @@ import ( "github.com/ooni/probe-cli/v3/internal/netxlite/quicx" ) -// QUICContextDialer is a dialer for QUIC using Context. -type QUICContextDialer interface { +// QUICDialer dials QUIC sessions. +type QUICDialer interface { // DialContext establishes a new QUIC session using the given // network and address. The tlsConfig and the quicConfig arguments // MUST NOT be nil. Returns either the session or an error. DialContext(ctx context.Context, network, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) + + // CloseIdleConnections closes idle connections, if any. + CloseIdleConnections() } // QUICListener listens for QUIC connections. @@ -47,12 +50,12 @@ type quicDialerQUICGo struct { quicConfig *quic.Config) (quic.EarlySession, error) } -var _ QUICContextDialer = &quicDialerQUICGo{} +var _ QUICDialer = &quicDialerQUICGo{} // errInvalidIP indicates that a string is not a valid IP. var errInvalidIP = errors.New("netxlite: invalid IP") -// DialContext implements ContextDialer.DialContext. This function will +// DialContext implements QUICDialer.DialContext. This function will // apply the following TLS defaults: // // 1. if tlsConfig.RootCAs is nil, we use the Mozilla CA that we @@ -119,6 +122,11 @@ func (d *quicDialerQUICGo) maybeApplyTLSDefaults(config *tls.Config, port int) * return config } +// CloseIdleConnections closes idle connections. +func (d *quicDialerQUICGo) CloseIdleConnections() { + // nothing to do +} + // quicSessionOwnsConn ensures that we close the UDPLikeConn. type quicSessionOwnsConn struct { // EarlySession is the embedded early session @@ -139,16 +147,16 @@ func (sess *quicSessionOwnsConn) CloseWithError( // quicDialerResolver is a dialer that uses the configured Resolver // to resolve a domain name to IP addrs. type quicDialerResolver struct { - // Dialer is the underlying QUIC dialer. - Dialer QUICContextDialer + // Dialer is the underlying QUICDialer. + Dialer QUICDialer - // Resolver is the underlying resolver. + // Resolver is the underlying Resolver. Resolver Resolver } -var _ QUICContextDialer = &quicDialerResolver{} +var _ QUICDialer = &quicDialerResolver{} -// DialContext implements QUICContextDialer.DialContext. This function +// DialContext implements QUICDialer.DialContext. This function // will apply the following TLS defaults: // // 1. if tlsConfig.ServerName is empty, we will use the hostname @@ -202,16 +210,22 @@ func (d *quicDialerResolver) lookupHost(ctx context.Context, hostname string) ([ return d.Resolver.LookupHost(ctx, hostname) } +// CloseIdleConnections implements QUICDialer.CloseIdleConnections. +func (d *quicDialerResolver) CloseIdleConnections() { + d.Dialer.CloseIdleConnections() + d.Resolver.CloseIdleConnections() +} + // quicDialerLogger is a dialer with logging. type quicDialerLogger struct { // Dialer is the underlying QUIC dialer. - Dialer QUICContextDialer + Dialer QUICDialer // Logger is the underlying logger. Logger Logger } -var _ QUICContextDialer = &quicDialerLogger{} +var _ QUICDialer = &quicDialerLogger{} // DialContext implements QUICContextDialer.DialContext. func (d *quicDialerLogger) DialContext( @@ -226,3 +240,8 @@ func (d *quicDialerLogger) DialContext( d.Logger.Debugf("quic %s/%s... ok", address, network) return sess, nil } + +// CloseIdleConnections implements QUICDialer.CloseIdleConnections. +func (d *quicDialerLogger) CloseIdleConnections() { + d.Dialer.CloseIdleConnections() +} diff --git a/internal/netxlite/quic_test.go b/internal/netxlite/quic_test.go index 5db4a02..844371a 100644 --- a/internal/netxlite/quic_test.go +++ b/internal/netxlite/quic_test.go @@ -22,6 +22,7 @@ func TestQUICDialerQUICGoCannotSplitHostPort(t *testing.T) { systemdialer := quicDialerQUICGo{ QUICListener: &quicListenerStdlib{}, } + defer systemdialer.CloseIdleConnections() // just to see it running ctx := context.Background() sess, err := systemdialer.DialContext( ctx, "udp", "a.b.c.d", tlsConfig, &quic.Config{}) @@ -212,6 +213,29 @@ func TestQUICDialerQUICGoTLSDefaultsForDoQ(t *testing.T) { } } +func TestQUICDialerResolverCloseIdleConnections(t *testing.T) { + var ( + forDialer bool + forResolver bool + ) + d := &quicDialerResolver{ + Dialer: &mocks.QUICDialer{ + MockCloseIdleConnections: func() { + forDialer = true + }, + }, + Resolver: &mocks.Resolver{ + MockCloseIdleConnections: func() { + forResolver = true + }, + }, + } + d.CloseIdleConnections() + if !forDialer || !forResolver { + t.Fatal("not called") + } +} + func TestQUICDialerResolverSuccess(t *testing.T) { tlsConfig := &tls.Config{} dialer := &quicDialerResolver{ @@ -313,7 +337,7 @@ func TestQUICDialerResolverApplyTLSDefaults(t *testing.T) { tlsConfig := &tls.Config{} dialer := &quicDialerResolver{ Resolver: NewResolverSystem(log.Log), - Dialer: &mocks.QUICContextDialer{ + Dialer: &mocks.QUICDialer{ MockDialContext: func(ctx context.Context, network, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) { gotTLSConfig = tlsConfig @@ -337,9 +361,24 @@ func TestQUICDialerResolverApplyTLSDefaults(t *testing.T) { } } +func TestQUICDialerLoggerCloseIdleConnections(t *testing.T) { + var forDialer bool + d := &quicDialerLogger{ + Dialer: &mocks.QUICDialer{ + MockCloseIdleConnections: func() { + forDialer = true + }, + }, + } + d.CloseIdleConnections() + if !forDialer { + t.Fatal("not called") + } +} + func TestQUICDialerLoggerSuccess(t *testing.T) { d := &quicDialerLogger{ - Dialer: &mocks.QUICContextDialer{ + Dialer: &mocks.QUICDialer{ MockDialContext: func(ctx context.Context, network string, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) { @@ -368,7 +407,7 @@ func TestQUICDialerLoggerSuccess(t *testing.T) { func TestQUICDialerLoggerFailure(t *testing.T) { expected := errors.New("mocked error") d := &quicDialerLogger{ - Dialer: &mocks.QUICContextDialer{ + Dialer: &mocks.QUICDialer{ MockDialContext: func(ctx context.Context, network string, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {