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) {