ooni-probe-cli/internal/netxlite/dialer.go
Simone Basso aa77867145
fix(netxlite): clearly document quirk and make code robust (#468)
This quirk really saddens me. It's a piece of tech debt we're
carrying over from the original netx implementation.

We cannot remove it _until_ we have legacy netx code around.

The second best thing we can do is to clearly move this code in
a place where it's clear it's a quirk and write and use some extra
code that makes sure the quirk's assumptions are always met.

Sigh.

See https://github.com/ooni/probe/issues/1591
2021-09-06 20:17:45 +02:00

180 lines
5.0 KiB
Go

package netxlite
import (
"context"
"errors"
"net"
"sync"
"time"
)
// Dialer establishes network connections.
type Dialer interface {
// DialContext behaves like net.Dialer.DialContext.
DialContext(ctx context.Context, network, address string) (net.Conn, error)
// CloseIdleConnections closes idle connections, if any.
CloseIdleConnections()
}
// NewDialerWithResolver creates a dialer using the given resolver and logger.
func NewDialerWithResolver(logger Logger, resolver Resolver) Dialer {
return &dialerLogger{
Dialer: &dialerResolver{
Dialer: &dialerLogger{
Dialer: &dialerSystem{},
Logger: logger,
},
Resolver: resolver,
},
Logger: logger,
}
}
// NewDialerWithoutResolver creates a dialer that uses the given
// logger and fails with ErrNoResolver when it is passed a domain name.
func NewDialerWithoutResolver(logger Logger) Dialer {
return NewDialerWithResolver(logger, &nullResolver{})
}
// underlyingDialer is the Dialer we use by default.
var underlyingDialer = &net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 15 * time.Second,
}
// dialerSystem dials using Go stdlib.
type dialerSystem struct{}
// DialContext implements Dialer.DialContext.
func (d *dialerSystem) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return underlyingDialer.DialContext(ctx, network, address)
}
// CloseIdleConnections implements Dialer.CloseIdleConnections.
func (d *dialerSystem) CloseIdleConnections() {
// nothing
}
var defaultDialer Dialer = &dialerSystem{}
// dialerResolver is a dialer that uses the configured Resolver to resolver a
// domain name to IP addresses, and the configured Dialer to connect.
type dialerResolver struct {
// Dialer is the underlying Dialer.
Dialer Dialer
// Resolver is the underlying Resolver.
Resolver Resolver
}
var _ Dialer = &dialerResolver{}
// DialContext implements Dialer.DialContext.
func (d *dialerResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
onlyhost, onlyport, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
addrs, err := d.lookupHost(ctx, onlyhost)
if err != nil {
return nil, err
}
// TODO(bassosimone): here we should be using multierror rather
// than just calling ReduceErrors. We are not ready to do that
// yet, though. To do that, we need first to modify nettests so
// that we actually avoid dialing when measuring.
//
// See also the quirks.go file. This is clearly a QUIRK.
addrs = quirkSortIPAddrs(addrs)
var errorslist []error
for _, addr := range addrs {
target := net.JoinHostPort(addr, onlyport)
conn, err := d.Dialer.DialContext(ctx, network, target)
if err == nil {
return conn, nil
}
errorslist = append(errorslist, err)
}
return nil, quirkReduceErrors(errorslist)
}
// lookupHost performs a domain name resolution.
func (d *dialerResolver) lookupHost(ctx context.Context, hostname string) ([]string, error) {
if net.ParseIP(hostname) != nil {
return []string{hostname}, nil
}
return d.Resolver.LookupHost(ctx, hostname)
}
// CloseIdleConnections implements Dialer.CloseIdleConnections.
func (d *dialerResolver) CloseIdleConnections() {
d.Dialer.CloseIdleConnections()
d.Resolver.CloseIdleConnections()
}
// dialerLogger is a Dialer with logging.
type dialerLogger struct {
// Dialer is the underlying dialer.
Dialer Dialer
// Logger is the underlying logger.
Logger Logger
}
var _ Dialer = &dialerLogger{}
// DialContext implements Dialer.DialContext
func (d *dialerLogger) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
d.Logger.Debugf("dial %s/%s...", address, network)
start := time.Now()
conn, err := d.Dialer.DialContext(ctx, network, address)
elapsed := time.Since(start)
if err != nil {
d.Logger.Debugf("dial %s/%s... %s in %s", address, network, err, elapsed)
return nil, err
}
d.Logger.Debugf("dial %s/%s... ok in %s", address, network, elapsed)
return conn, nil
}
// CloseIdleConnections implements Dialer.CloseIdleConnections.
func (d *dialerLogger) CloseIdleConnections() {
d.Dialer.CloseIdleConnections()
}
// ErrNoConnReuse indicates we cannot reuse the connection provided
// to a single use (possibly TLS) dialer.
var ErrNoConnReuse = errors.New("cannot reuse connection")
// NewSingleUseDialer returns a dialer that returns the given connection once
// and after that always fails with the ErrNoConnReuse error.
func NewSingleUseDialer(conn net.Conn) Dialer {
return &dialerSingleUse{conn: conn}
}
// dialerSingleUse is the type of Dialer returned by NewSingleDialer.
type dialerSingleUse struct {
sync.Mutex
conn net.Conn
}
var _ Dialer = &dialerSingleUse{}
// DialContext implements Dialer.DialContext.
func (s *dialerSingleUse) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
defer s.Unlock()
s.Lock()
if s.conn == nil {
return nil, ErrNoConnReuse
}
var conn net.Conn
conn, s.conn = s.conn, nil
return conn, nil
}
// CloseIdleConnections closes idle connections.
func (s *dialerSingleUse) CloseIdleConnections() {
// nothing
}