From dd5655eaee3ae760072d1757cb8ee038552386db Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Tue, 31 May 2022 20:28:25 +0200 Subject: [PATCH] refactor(netxlite): allow easy QUIC dialer chain customization (#771) Like the previous diff, but for creating QUIC dialers. See https://github.com/ooni/probe/issues/2121. --- internal/engine/netx/netx.go | 26 +++++++------- internal/netxlite/dialer.go | 2 +- internal/netxlite/quic.go | 62 +++++++++++++++------------------- internal/netxlite/quic_test.go | 22 ++++++++++-- 4 files changed, 60 insertions(+), 52 deletions(-) diff --git a/internal/engine/netx/netx.go b/internal/engine/netx/netx.go index d9e84c713e..c76ba3385e 100644 --- a/internal/engine/netx/netx.go +++ b/internal/engine/netx/netx.go @@ -131,28 +131,26 @@ func NewQUICDialer(config Config) model.QUICDialer { if config.FullResolver == nil { config.FullResolver = NewResolver(config) } - var ql model.QUICListener = &netxlite.QUICListenerStdlib{} - ql = &netxlite.ErrorWrapperQUICListener{QUICListener: ql} + ql := netxlite.NewQUICListener() if config.ReadWriteSaver != nil { ql = &quicdialer.QUICListenerSaver{ QUICListener: ql, Saver: config.ReadWriteSaver, } } - var d model.QUICDialer = &netxlite.QUICDialerQUICGo{ - QUICListener: ql, - } - d = &netxlite.ErrorWrapperQUICDialer{ - QUICDialer: d, - } - if config.TLSSaver != nil { - d = quicdialer.HandshakeSaver{Saver: config.TLSSaver, QUICDialer: d} + var logger model.DebugLogger = model.DiscardLogger + if config.Logger != nil { + logger = config.Logger } - d = &netxlite.QUICDialerResolver{ - Resolver: config.FullResolver, - Dialer: d, + extensions := []netxlite.QUICDialerWrapper{ + func(dialer model.QUICDialer) model.QUICDialer { + if config.TLSSaver != nil { + dialer = quicdialer.HandshakeSaver{Saver: config.TLSSaver, QUICDialer: dialer} + } + return dialer + }, } - return d + return netxlite.NewQUICDialerWithResolver(ql, logger, config.FullResolver, extensions...) } // NewTLSDialer creates a new TLSDialer from the specified config diff --git a/internal/netxlite/dialer.go b/internal/netxlite/dialer.go index a0e3c5eca3..48f2a4c2a8 100644 --- a/internal/netxlite/dialer.go +++ b/internal/netxlite/dialer.go @@ -135,7 +135,7 @@ func NewDialerWithoutResolver(dl model.DebugLogger, w ...DialerWrapper) model.Di return NewDialerWithResolver(dl, &NullResolver{}, w...) } -// DialerSystem is a model.Dialer that users TProxy.NewSimplerDialer +// DialerSystem is a model.Dialer that uses TProxy.NewSimplerDialer // to construct the new SimpleDialer used for dialing. This dialer has // a fixed timeout for each connect operation equal to 15 seconds. type DialerSystem struct { diff --git a/internal/netxlite/quic.go b/internal/netxlite/quic.go index 9e41f08007..a47d386dde 100644 --- a/internal/netxlite/quic.go +++ b/internal/netxlite/quic.go @@ -32,39 +32,32 @@ func (qls *quicListenerStdlib) Listen(addr *net.UDPAddr) (model.UDPLikeConn, err return TProxy.ListenUDP("udp", addr) } -// NewQUICDialerWithResolver returns a QUICDialer using the given -// QUICListener to create listening connections and the given Resolver -// to resolve domain names (if needed). -// -// Properties of the dialer: -// -// 1. logs events using the given logger; -// -// 2. resolves domain names using the givern resolver; -// -// 3. when using a resolver, _may_ attempt multiple dials -// in parallel (happy eyeballs) and _may_ return an aggregate -// error to the caller; -// -// 4. wraps errors; -// -// 5. has a configured connect timeout; +// QUICDialerWrapper is a function that allows you to customize the kind of QUICDialer +// returned by NewQUICDialerWithResolver and NewQUICDialerWithoutResolver. +type QUICDialerWrapper func(dialer model.QUICDialer) model.QUICDialer + +// NewQUICDialerWithResolver is the WrapDialer equivalent for QUIC where +// we return a composed QUICDialer modified by optional wrappers. // -// 6. if a dialer wraps a resolver, the dialer will forward -// the CloseIdleConnection call to its resolver (which is -// instrumental to manage a DoH resolver connections properly). -func NewQUICDialerWithResolver(listener model.QUICListener, - logger model.DebugLogger, resolver model.Resolver) model.QUICDialer { +// Unlike the dialer returned by WrapDialer, this dialer MAY attempt +// happy eyeballs, perform parallel dial attempts, and return an error +// that aggregates all the errors that occurred. +func NewQUICDialerWithResolver(listener model.QUICListener, logger model.DebugLogger, + resolver model.Resolver, wrappers ...QUICDialerWrapper) (outDialer model.QUICDialer) { + outDialer = &quicDialerErrWrapper{ + QUICDialer: &quicDialerHandshakeCompleter{ + Dialer: &quicDialerQUICGo{ + QUICListener: listener, + }, + }, + } + for _, wrapper := range wrappers { + outDialer = wrapper(outDialer) // extend with user-supplied constructors + } return &quicDialerLogger{ Dialer: &quicDialerResolver{ Dialer: &quicDialerLogger{ - Dialer: &quicDialerErrWrapper{ - QUICDialer: &quicDialerHandshakeCompleter{ - Dialer: &quicDialerQUICGo{ - QUICListener: listener, - }, - }, - }, + Dialer: outDialer, Logger: logger, operationSuffix: "_address", }, @@ -74,12 +67,11 @@ func NewQUICDialerWithResolver(listener model.QUICListener, } } -// NewQUICDialerWithoutResolver is like NewQUICDialerWithResolver -// except that there is no configured resolver. So, if you pass in -// an address containing a domain name, the dial will fail with -// the ErrNoResolver failure. -func NewQUICDialerWithoutResolver(listener model.QUICListener, logger model.DebugLogger) model.QUICDialer { - return NewQUICDialerWithResolver(listener, logger, &NullResolver{}) +// NewQUICDialerWithoutResolver is equivalent to calling NewQUICDialerWithResolver +// with the resolver argument set to &NullResolver{}. +func NewQUICDialerWithoutResolver(listener model.QUICListener, + logger model.DebugLogger, wrappers ...QUICDialerWrapper) model.QUICDialer { + return NewQUICDialerWithResolver(listener, logger, &NullResolver{}, wrappers...) } // quicDialerQUICGo dials using the lucas-clemente/quic-go library. diff --git a/internal/netxlite/quic_test.go b/internal/netxlite/quic_test.go index 56951e485f..e730e2ee1b 100644 --- a/internal/netxlite/quic_test.go +++ b/internal/netxlite/quic_test.go @@ -22,9 +22,25 @@ func TestNewQUICListener(t *testing.T) { _ = qew.QUICListener.(*quicListenerStdlib) } +type extensionQUICDialerFirst struct { + model.QUICDialer +} + +type extensionQUICDialerSecond struct { + model.QUICDialer +} + func TestNewQUICDialer(t *testing.T) { ql := NewQUICListener() - dlr := NewQUICDialerWithoutResolver(ql, log.Log) + extensions := []QUICDialerWrapper{ + func(dialer model.QUICDialer) model.QUICDialer { + return &extensionQUICDialerFirst{dialer} + }, + func(dialer model.QUICDialer) model.QUICDialer { + return &extensionQUICDialerSecond{dialer} + }, + } + dlr := NewQUICDialerWithoutResolver(ql, log.Log, extensions...) logger := dlr.(*quicDialerLogger) if logger.Logger != log.Log { t.Fatal("invalid logger") @@ -37,7 +53,9 @@ func TestNewQUICDialer(t *testing.T) { if logger.Logger != log.Log { t.Fatal("invalid logger") } - errWrapper := logger.Dialer.(*quicDialerErrWrapper) + ext2 := logger.Dialer.(*extensionQUICDialerSecond) + ext1 := ext2.QUICDialer.(*extensionQUICDialerFirst) + errWrapper := ext1.QUICDialer.(*quicDialerErrWrapper) handshakeCompleter := errWrapper.QUICDialer.(*quicDialerHandshakeCompleter) base := handshakeCompleter.Dialer.(*quicDialerQUICGo) if base.QUICListener != ql {