3cd88debdc
* netxlite: improve docs, tests, and code quality * better documentation * more strict testing of dialer (especially make sure we document the quirk in https://github.com/ooni/probe/issues/1779 and we have tests to guarantee we don't screw up here) * introduce NewErrWrapper factory for creating errors so we have confidence we are creating them correctly Part of https://github.com/ooni/probe/issues/1591
281 lines
7.6 KiB
Go
281 lines
7.6 KiB
Go
package netxlite
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite/errorsx"
|
|
)
|
|
|
|
// 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 new Dialer. The returned Dialer
|
|
// has the following properties:
|
|
//
|
|
// 1. logs events using the given logger;
|
|
//
|
|
// 2. resolves domain names using the givern resolver;
|
|
//
|
|
// 3. when using a resolver, each available enpoint is tried
|
|
// sequentially. On error, the code will return what it believes
|
|
// to be the most representative error in the pack. Most often,
|
|
// such an error is the first one that occurred. Choosing the
|
|
// error to return using this logic is a QUIRK that we owe
|
|
// to the original implementation of netx. We cannot change
|
|
// this behavior until all the legacy code that relies on
|
|
// it has been migrated to more sane patterns.
|
|
//
|
|
// Removing this quirk from the codebase is documented as
|
|
// TODO(https://github.com/ooni/probe/issues/1779).
|
|
//
|
|
// 4. wraps errors;
|
|
//
|
|
// 5. has a configured connect timeout.
|
|
func NewDialerWithResolver(logger Logger, resolver Resolver) Dialer {
|
|
return &dialerLogger{
|
|
Dialer: &dialerResolver{
|
|
Dialer: &dialerLogger{
|
|
Dialer: &dialerErrWrapper{
|
|
Dialer: &dialerSystem{},
|
|
},
|
|
Logger: logger,
|
|
operationSuffix: "_address",
|
|
},
|
|
Resolver: resolver,
|
|
},
|
|
Logger: logger,
|
|
}
|
|
}
|
|
|
|
// NewDialerWithoutResolver is like NewDialerWithResolver except that
|
|
// it will fail with ErrNoResolver if passed a domain name.
|
|
func NewDialerWithoutResolver(logger Logger) Dialer {
|
|
return NewDialerWithResolver(logger, &nullResolver{})
|
|
}
|
|
|
|
// dialerSystem uses system facilities to perform domain name
|
|
// resolution and guarantees we have a dialer timeout.
|
|
type dialerSystem struct {
|
|
// timeout is the OPTIONAL timeout used for testing.
|
|
timeout time.Duration
|
|
}
|
|
|
|
var _ Dialer = &dialerSystem{}
|
|
|
|
const dialerDefaultTimeout = 15 * time.Second
|
|
|
|
func (d *dialerSystem) newUnderlyingDialer() *net.Dialer {
|
|
t := d.timeout
|
|
if t <= 0 {
|
|
t = dialerDefaultTimeout
|
|
}
|
|
return &net.Dialer{Timeout: t}
|
|
}
|
|
|
|
func (d *dialerSystem) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return d.newUnderlyingDialer().DialContext(ctx, network, address)
|
|
}
|
|
|
|
func (d *dialerSystem) CloseIdleConnections() {
|
|
// nothing to do here
|
|
}
|
|
|
|
// dialerResolver combines dialing with domain name resolution.
|
|
type dialerResolver struct {
|
|
Dialer
|
|
Resolver
|
|
}
|
|
|
|
var _ Dialer = &dialerResolver{}
|
|
|
|
func (d *dialerResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
// QUIRK: this routine and the related routines in quirks.go cannot
|
|
// be changed easily until we use events tracing to measure.
|
|
//
|
|
// Reference issue: TODO(https://github.com/ooni/probe/issues/1779).
|
|
onlyhost, onlyport, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addrs, err := d.lookupHost(ctx, onlyhost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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 ensures we correctly handle IP addresses.
|
|
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)
|
|
}
|
|
|
|
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
|
|
|
|
// Logger is the underlying logger.
|
|
Logger
|
|
|
|
// operationSuffix is appended to the operation name.
|
|
//
|
|
// We use this suffix to distinguish the output from dialing
|
|
// with the output from dialing an IP address when we are
|
|
// using a dialer without resolver, where otherwise both lines
|
|
// would read something like `dial 8.8.8.8:443...`
|
|
operationSuffix string
|
|
}
|
|
|
|
var _ Dialer = &dialerLogger{}
|
|
|
|
func (d *dialerLogger) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
d.Logger.Debugf("dial%s %s/%s...", d.operationSuffix, 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... %s in %s", d.operationSuffix,
|
|
address, network, err, elapsed)
|
|
return nil, err
|
|
}
|
|
d.Logger.Debugf("dial%s %s/%s... ok in %s", d.operationSuffix,
|
|
address, network, elapsed)
|
|
return conn, nil
|
|
}
|
|
|
|
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 Dialer returned by NewSingleDialer.
|
|
type dialerSingleUse struct {
|
|
sync.Mutex
|
|
conn net.Conn
|
|
}
|
|
|
|
var _ Dialer = &dialerSingleUse{}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *dialerSingleUse) CloseIdleConnections() {
|
|
// nothing to do
|
|
}
|
|
|
|
// dialerErrWrapper is a dialer that performs error wrapping. The connection
|
|
// returned by the DialContext function will also perform error wrapping.
|
|
type dialerErrWrapper struct {
|
|
Dialer
|
|
}
|
|
|
|
var _ Dialer = &dialerErrWrapper{}
|
|
|
|
func (d *dialerErrWrapper) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
conn, err := d.Dialer.DialContext(ctx, network, address)
|
|
if err != nil {
|
|
return nil, errorsx.NewErrWrapper(
|
|
errorsx.ClassifyGenericError, errorsx.ConnectOperation, err)
|
|
}
|
|
return &dialerErrWrapperConn{Conn: conn}, nil
|
|
}
|
|
|
|
// dialerErrWrapperConn is a net.Conn that performs error wrapping.
|
|
type dialerErrWrapperConn struct {
|
|
net.Conn
|
|
}
|
|
|
|
var _ net.Conn = &dialerErrWrapperConn{}
|
|
|
|
func (c *dialerErrWrapperConn) Read(b []byte) (int, error) {
|
|
count, err := c.Conn.Read(b)
|
|
if err != nil {
|
|
return 0, errorsx.NewErrWrapper(
|
|
errorsx.ClassifyGenericError, errorsx.ReadOperation, err)
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func (c *dialerErrWrapperConn) Write(b []byte) (int, error) {
|
|
count, err := c.Conn.Write(b)
|
|
if err != nil {
|
|
return 0, errorsx.NewErrWrapper(
|
|
errorsx.ClassifyGenericError, errorsx.WriteOperation, err)
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func (c *dialerErrWrapperConn) Close() error {
|
|
err := c.Conn.Close()
|
|
if err != nil {
|
|
return errorsx.NewErrWrapper(
|
|
errorsx.ClassifyGenericError, errorsx.CloseOperation, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ErrNoDialer indicates that no dialer is configured.
|
|
var ErrNoDialer = errors.New("no configured dialer")
|
|
|
|
// NewNullDialer returns a dialer that always fails.
|
|
func NewNullDialer() Dialer {
|
|
return &nullDialer{}
|
|
}
|
|
|
|
type nullDialer struct{}
|
|
|
|
var _ Dialer = &nullDialer{}
|
|
|
|
func (*nullDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return nil, ErrNoDialer
|
|
}
|
|
|
|
func (*nullDialer) CloseIdleConnections() {
|
|
// nothing to do
|
|
}
|