package netxlite // // Code for dialing TCP or UDP net.Conn-like connections // import ( "context" "errors" "net" "sync" "time" "github.com/ooni/probe-cli/v3/internal/model" ) // NewDialerWithStdlibResolver is equivalent to creating a system resolver // using NewStdlibResolver and then a dialer using NewDialerWithResolver where // the resolver argument is the previously created resolver. func NewDialerWithStdlibResolver(dl model.DebugLogger) model.Dialer { reso := NewStdlibResolver(dl) return NewDialerWithResolver(dl, reso) } // NewDialerWithResolver is equivalent to calling WrapDialer with // the dialer argument being equal to &DialerSystem{}. func NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer { return WrapDialer(dl, r, &DialerSystem{}, w...) } // WrapDialer wraps an existing Dialer to add extra functionality // such as separting DNS lookup and connecting, error wrapping, logging, etc. // // When possible use NewDialerWithResolver or NewDialerWithoutResolver // instead of using this rather low-level function. // // # Arguments // // 1. logger is used to emit debug messages (MUST NOT be nil); // // 2. resolver is the resolver to use when dialing for endpoint // addresses containing domain names (MUST NOT be nil); // // 3. baseDialer is the dialer to wrap (MUST NOT be nil); // // 4. wrappers is a list of zero or more functions allowing you to // modify the behavior of the returned dialer (see below). Please note // that this function will just ignore any nil wrapper. // // # Return value // // The returned dialer is an opaque type consisting of the composition of // several simple dialers. The following pseudo code illustrates the general // behavior of the returned composed dialer: // // addrs, err := dnslookup() // if err != nil { // return nil, err // } // errors := []error{} // for _, a := range addrs { // conn, err := tcpconnect(a) // if err != nil { // errors = append(errors, err) // continue // } // return conn, nil // } // return nil, errors[0] // // The following table describes the structure of the returned dialer: // // +-------+-----------------+------------------------------------------+ // | Index | Name | Description | // +-------+-----------------+------------------------------------------+ // | 0 | base | the baseDialer argument | // +-------+-----------------+------------------------------------------+ // | 1 | errWrapper | wraps Go errors to be consistent with | // | | | OONI df-007-errors spec | // +-------+-----------------+------------------------------------------+ // | 2 | ??? | if there are wrappers, result of calling | // | | | the first one on the errWrapper dialer | // +-------+-----------------+------------------------------------------+ // | ... | ... | ... | // +-------+-----------------+------------------------------------------+ // | N | ??? | if there are wrappers, result of calling | // | | | the last one on the N-1 dialer | // +-------+-----------------+------------------------------------------+ // | N+1 | logger (inner) | logs TCP connect operations | // +-------+-----------------+------------------------------------------+ // | N+2 | resolver | DNS lookup and try connect each IP in | // | | | sequence until one of them succeeds | // +-------+-----------------+------------------------------------------+ // | N+3 | logger (outer) | logs the overall dial operation | // +-------+-----------------+------------------------------------------+ // // The list of wrappers allows to insert modified dialers in the correct // place for observing and saving I/O events (connect, read, etc.). // // # Remarks // // When the resolver is &NullResolver{} any attempt to perform DNS resolutions // in the dialer at index N+2 will fail with ErrNoResolver. // // Otherwise, the dialer at index N+2 will try each resolver IP address // sequentially. In case of failure, such a resolver will return the first // error that occurred. This implementation strategy is a QUIRK that is // documented at TODO(https://github.com/ooni/probe/issues/1779). // // If the baseDialer is &DialerSystem{}, there will be a fixed TCP connect // timeout for each connect operation. Because there may be multiple IP // addresses per dial, the overall timeout would be a multiple of the timeout // of a single connect operation. You may want to use the context to reduce // the overall time spent trying all addresses and timing out. func WrapDialer(logger model.DebugLogger, resolver model.Resolver, baseDialer model.Dialer, wrappers ...model.DialerWrapper) (outDialer model.Dialer) { outDialer = &dialerErrWrapper{ Dialer: baseDialer, } for _, wrapper := range wrappers { if wrapper == nil { continue // ignore as documented } outDialer = wrapper.WrapDialer(outDialer) // extend with user-supplied constructors } return &dialerLogger{ Dialer: &dialerResolverWithTracing{ Dialer: &dialerLogger{ Dialer: outDialer, DebugLogger: logger, operationSuffix: "_address", }, Resolver: resolver, }, DebugLogger: logger, } } // NewDialerWithoutResolver is equivalent to calling NewDialerWithResolver // with the resolver argument being &NullResolver{}. func NewDialerWithoutResolver(dl model.DebugLogger, w ...model.DialerWrapper) model.Dialer { return NewDialerWithResolver(dl, &NullResolver{}, w...) } // DialerSystem is a model.Dialer that uses the stdlib's net.Dialer // 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 { // timeout is the OPTIONAL timeout (for testing). timeout time.Duration } var _ model.Dialer = &DialerSystem{} const dialerDefaultTimeout = 15 * time.Second func (d *DialerSystem) configuredTimeout() time.Duration { t := d.timeout if t <= 0 { t = dialerDefaultTimeout } return t } func (d *DialerSystem) DialContext(ctx context.Context, network, address string) (net.Conn, error) { return TProxy.DialContext(ctx, d.configuredTimeout(), network, address) } func (d *DialerSystem) CloseIdleConnections() { // nothing to do here } // dialerResolverWithTracing combines dialing with domain name resolution and // implements hooks to trace TCP (or UDP) connect operations. type dialerResolverWithTracing struct { Dialer model.Dialer Resolver model.Resolver } var _ model.Dialer = &dialerResolverWithTracing{} // DialContext implements model.Dialer.DialContext. Specifically this // method performs the following operations: // // 1. resolve the domain inside the address using a resolver; // // 2. cycle through the available IP addresses and try to dial each of them; // // 3. trace the TCP (or UDP) connect and allow wrapping the returned conn. func (d *dialerResolverWithTracing) 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 trace := ContextTraceOrDefault(ctx) for _, addr := range addrs { target := net.JoinHostPort(addr, onlyport) started := trace.TimeNow() conn, err := d.Dialer.DialContext(ctx, network, target) finished := trace.TimeNow() // TODO(bassosimone): to make the code robust to future refactoring we have // moved error wrapping inside this type. This change opens up the possibility // of simplifying the dialing chain by removing dialerErrWrapper. We'll be // able to implement this refactoring once netx is gone. We cannot complete // this refactoring _before_ because WrapDialer inserts extra wrappers // provided by netx in the dialers chain _before_ this dialer and the dialers // that netx insert assume that they wrap a dialer with error wrapping. // // Because error wrapping should be idempotent, it should not be a problem // to have two error wrapping dialers in the chain except that, of course, it // would be less efficient than just having a single wrapper. err = MaybeNewErrWrapper(ClassifyGenericError, ConnectOperation, err) trace.OnConnectDone(started, network, onlyhost, target, err, finished) if err == nil { conn = &dialerErrWrapperConn{conn} return trace.MaybeWrapNetConn(conn), nil } errorslist = append(errorslist, err) } return nil, quirkReduceErrors(errorslist) } // lookupHost ensures we correctly handle IP addresses. func (d *dialerResolverWithTracing) 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 *dialerResolverWithTracing) CloseIdleConnections() { d.Dialer.CloseIdleConnections() d.Resolver.CloseIdleConnections() } // dialerLogger is a Dialer with logging. type dialerLogger struct { // Dialer is the underlying dialer. Dialer model.Dialer // DebugLogger is the underlying logger. DebugLogger model.DebugLogger // 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 _ model.Dialer = &dialerLogger{} func (d *dialerLogger) DialContext(ctx context.Context, network, address string) (net.Conn, error) { d.DebugLogger.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.DebugLogger.Debugf("dial%s %s/%s... %s in %s", d.operationSuffix, address, network, err, elapsed) return nil, err } d.DebugLogger.Debugf("dial%s %s/%s... ok in %s", d.operationSuffix, address, network, elapsed) return conn, nil } func (d *dialerLogger) CloseIdleConnections() { d.Dialer.CloseIdleConnections() } // ErrNoConnReuse is the type of error returned when you create a // "single use" dialer or a "single use" TLS dialer and you dial // more than once, which is not supported by such a dialer. var ErrNoConnReuse = errors.New("cannot reuse connection") // NewSingleUseDialer returns a "single use" dialer. The first // dial will succed and return conn regardless of the network // and address arguments passed to DialContext. Any subsequent // dial returns ErrNoConnReuse. func NewSingleUseDialer(conn net.Conn) model.Dialer { return &dialerSingleUse{conn: conn} } // dialerSingleUse is the Dialer returned by NewSingleDialer. type dialerSingleUse struct { mu sync.Mutex conn net.Conn } var _ model.Dialer = &dialerSingleUse{} func (s *dialerSingleUse) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) { defer s.mu.Unlock() s.mu.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 model.Dialer } var _ model.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, NewErrWrapper(ClassifyGenericError, ConnectOperation, err) } return &dialerErrWrapperConn{Conn: conn}, nil } func (d *dialerErrWrapper) CloseIdleConnections() { d.Dialer.CloseIdleConnections() } // 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, NewErrWrapper(ClassifyGenericError, ReadOperation, err) } return count, nil } func (c *dialerErrWrapperConn) Write(b []byte) (int, error) { count, err := c.Conn.Write(b) if err != nil { return 0, NewErrWrapper(ClassifyGenericError, WriteOperation, err) } return count, nil } func (c *dialerErrWrapperConn) Close() error { err := c.Conn.Close() if err != nil { return NewErrWrapper(ClassifyGenericError, CloseOperation, err) } return nil } // ErrNoDialer is the type of error returned by "null" dialers // when you attempt to dial with them. var ErrNoDialer = errors.New("no configured dialer") // NewNullDialer returns a dialer that always fails with ErrNoDialer. func NewNullDialer() model.Dialer { return &nullDialer{} } type nullDialer struct{} var _ model.Dialer = &nullDialer{} func (*nullDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { return nil, ErrNoDialer } func (*nullDialer) CloseIdleConnections() { // nothing to do }