86ffd6a0c4
We originally removed the TProxy in https://github.com/ooni/probe/issues/2224. Nevertheless, in https://github.com/ooni/probe-cli/pull/969, we determined that something like the previous TProxy, with small changes, was required to support https://github.com/ooni/probe/issues/2340. So, this pull request reintroduces a slightly-modified TProxy functionality that better adapts to the `--remote=REMOTE` use case.
452 lines
14 KiB
Go
452 lines
14 KiB
Go
package netxlite
|
|
|
|
//
|
|
// QUIC implementation
|
|
//
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/lucas-clemente/quic-go"
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
)
|
|
|
|
// NewQUICListener creates a new QUICListener using the standard
|
|
// library to create listening UDP sockets.
|
|
func NewQUICListener() model.QUICListener {
|
|
return &quicListenerErrWrapper{&quicListenerStdlib{}}
|
|
}
|
|
|
|
// quicListenerStdlib is a QUICListener using the standard library.
|
|
type quicListenerStdlib struct{}
|
|
|
|
var _ model.QUICListener = &quicListenerStdlib{}
|
|
|
|
// Listen implements QUICListener.Listen.
|
|
func (qls *quicListenerStdlib) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) {
|
|
return TProxy.ListenUDP("udp", addr)
|
|
}
|
|
|
|
// NewQUICDialerWithResolver is the WrapDialer equivalent for QUIC where
|
|
// we return a composed QUICDialer modified by optional wrappers.
|
|
//
|
|
// Please, note that this fuunction will just ignore any nil wrapper.
|
|
//
|
|
// 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 ...model.QUICDialerWrapper) (outDialer model.QUICDialer) {
|
|
outDialer = &quicDialerErrWrapper{
|
|
QUICDialer: &quicDialerHandshakeCompleter{
|
|
Dialer: &quicDialerQUICGo{
|
|
QUICListener: listener,
|
|
},
|
|
},
|
|
}
|
|
for _, wrapper := range wrappers {
|
|
if wrapper == nil {
|
|
continue // ignore as documented
|
|
}
|
|
outDialer = wrapper.WrapQUICDialer(outDialer) // extend with user-supplied constructors
|
|
}
|
|
return &quicDialerLogger{
|
|
Dialer: &quicDialerResolver{
|
|
Dialer: &quicDialerLogger{
|
|
Dialer: outDialer,
|
|
Logger: logger,
|
|
operationSuffix: "_address",
|
|
},
|
|
Resolver: resolver,
|
|
},
|
|
Logger: logger,
|
|
}
|
|
}
|
|
|
|
// NewQUICDialerWithoutResolver is equivalent to calling NewQUICDialerWithResolver
|
|
// with the resolver argument set to &NullResolver{}.
|
|
func NewQUICDialerWithoutResolver(listener model.QUICListener,
|
|
logger model.DebugLogger, wrappers ...model.QUICDialerWrapper) model.QUICDialer {
|
|
return NewQUICDialerWithResolver(listener, logger, &NullResolver{}, wrappers...)
|
|
}
|
|
|
|
// quicDialerQUICGo dials using the lucas-clemente/quic-go library.
|
|
type quicDialerQUICGo struct {
|
|
// QUICListener is the underlying QUICListener to use.
|
|
QUICListener model.QUICListener
|
|
|
|
// mockDialEarlyContext allows to mock quic.DialEarlyContext.
|
|
mockDialEarlyContext func(ctx context.Context, pconn net.PacketConn,
|
|
remoteAddr net.Addr, host string, tlsConfig *tls.Config,
|
|
quicConfig *quic.Config) (quic.EarlyConnection, error)
|
|
}
|
|
|
|
var _ model.QUICDialer = &quicDialerQUICGo{}
|
|
|
|
// ErrInvalidIP indicates that a string is not a valid IP.
|
|
var ErrInvalidIP = errors.New("netxlite: invalid IP")
|
|
|
|
// ParseUDPAddr maps the string representation of an UDP endpoint to the
|
|
// corresponding *net.UDPAddr representation.
|
|
func ParseUDPAddr(address string) (*net.UDPAddr, error) {
|
|
addr, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ipAddr := net.ParseIP(addr)
|
|
if ipAddr == nil {
|
|
return nil, ErrInvalidIP
|
|
}
|
|
dport, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
udpAddr := &net.UDPAddr{
|
|
IP: ipAddr,
|
|
Port: dport,
|
|
Zone: "",
|
|
}
|
|
return udpAddr, nil
|
|
}
|
|
|
|
// 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
|
|
// bundle with this measurement library;
|
|
//
|
|
// 2. if tlsConfig.NextProtos is empty _and_ the port is 443 or 8853,
|
|
// then we configure, respectively, "h3" and "dq".
|
|
func (d *quicDialerQUICGo) DialContext(ctx context.Context,
|
|
address string, tlsConfig *tls.Config, quicConfig *quic.Config) (
|
|
quic.EarlyConnection, error) {
|
|
udpAddr, err := ParseUDPAddr(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pconn, err := d.QUICListener.Listen(&net.UDPAddr{IP: net.IPv4zero, Port: 0, Zone: ""})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tlsConfig = d.maybeApplyTLSDefaults(tlsConfig, udpAddr.Port)
|
|
trace := ContextTraceOrDefault(ctx)
|
|
pconn = trace.MaybeWrapUDPLikeConn(pconn)
|
|
started := trace.TimeNow()
|
|
trace.OnQUICHandshakeStart(started, address, quicConfig)
|
|
qconn, err := d.dialEarlyContext(
|
|
ctx, pconn, udpAddr, address, tlsConfig, quicConfig)
|
|
finished := trace.TimeNow()
|
|
err = MaybeNewErrWrapper(ClassifyQUICHandshakeError, QUICHandshakeOperation, err)
|
|
trace.OnQUICHandshakeDone(started, address, qconn, tlsConfig, err, finished)
|
|
if err != nil {
|
|
pconn.Close() // we own it on failure
|
|
return nil, err
|
|
}
|
|
return newQUICConnectionOwnsConn(qconn, pconn), nil
|
|
}
|
|
|
|
func (d *quicDialerQUICGo) dialEarlyContext(ctx context.Context,
|
|
pconn net.PacketConn, remoteAddr net.Addr, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
|
|
if d.mockDialEarlyContext != nil {
|
|
return d.mockDialEarlyContext(
|
|
ctx, pconn, remoteAddr, address, tlsConfig, quicConfig)
|
|
}
|
|
return quic.DialEarlyContext(
|
|
ctx, pconn, remoteAddr, address, tlsConfig, quicConfig)
|
|
}
|
|
|
|
// maybeApplyTLSDefaults ensures that we're using our certificate pool, if
|
|
// needed, and that we use a suitable ALPN, if needed, for h3 and dq.
|
|
func (d *quicDialerQUICGo) maybeApplyTLSDefaults(config *tls.Config, port int) *tls.Config {
|
|
config = config.Clone()
|
|
if config.RootCAs == nil {
|
|
config.RootCAs = defaultCertPool
|
|
}
|
|
if len(config.NextProtos) <= 0 {
|
|
switch port {
|
|
case 443:
|
|
config.NextProtos = []string{"h3"}
|
|
case 8853:
|
|
// See https://datatracker.ietf.org/doc/html/draft-ietf-dprive-dnsoquic-02#section-10
|
|
config.NextProtos = []string{"dq"}
|
|
}
|
|
}
|
|
return config
|
|
}
|
|
|
|
// CloseIdleConnections closes idle connections.
|
|
func (d *quicDialerQUICGo) CloseIdleConnections() {
|
|
// nothing to do
|
|
}
|
|
|
|
// quicDialerHandshakeCompleter ensures we complete the handshake.
|
|
type quicDialerHandshakeCompleter struct {
|
|
Dialer model.QUICDialer
|
|
}
|
|
|
|
var _ model.QUICDialer = &quicDialerHandshakeCompleter{}
|
|
|
|
// DialContext implements model.QUICDialer.DialContext.
|
|
func (d *quicDialerHandshakeCompleter) DialContext(
|
|
ctx context.Context, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
|
|
conn, err := d.Dialer.DialContext(ctx, address, tlsConfig, quicConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
select {
|
|
case <-conn.HandshakeComplete().Done():
|
|
return conn, nil
|
|
case <-ctx.Done():
|
|
conn.CloseWithError(0, "") // we own the conn
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// CloseIdleConnections implements model.QUICDialer.CloseIdleConnections.
|
|
func (d *quicDialerHandshakeCompleter) CloseIdleConnections() {
|
|
d.Dialer.CloseIdleConnections()
|
|
}
|
|
|
|
// quicConnectionOwnsConn ensures that we close the UDPLikeConn.
|
|
type quicConnectionOwnsConn struct {
|
|
// EarlyConnection is the embedded early connection
|
|
quic.EarlyConnection
|
|
|
|
// conn is the connection we own
|
|
conn model.UDPLikeConn
|
|
}
|
|
|
|
func newQUICConnectionOwnsConn(qconn quic.EarlyConnection, pconn model.UDPLikeConn) *quicConnectionOwnsConn {
|
|
return &quicConnectionOwnsConn{EarlyConnection: qconn, conn: pconn}
|
|
}
|
|
|
|
// CloseWithError implements quic.EarlyConnection.CloseWithError.
|
|
func (qconn *quicConnectionOwnsConn) CloseWithError(
|
|
code quic.ApplicationErrorCode, reason string) error {
|
|
err := qconn.EarlyConnection.CloseWithError(code, reason)
|
|
qconn.conn.Close()
|
|
return err
|
|
}
|
|
|
|
// quicDialerResolver is a dialer that uses the configured Resolver
|
|
// to resolve a domain name to IP addrs.
|
|
type quicDialerResolver struct {
|
|
// Dialer is the underlying QUICDialer.
|
|
Dialer model.QUICDialer
|
|
|
|
// Resolver is the underlying Resolver.
|
|
Resolver model.Resolver
|
|
}
|
|
|
|
var _ model.QUICDialer = &quicDialerResolver{}
|
|
|
|
// DialContext implements QUICDialer.DialContext. This function
|
|
// will apply the following TLS defaults:
|
|
//
|
|
// 1. if tlsConfig.ServerName is empty, we will use the hostname
|
|
// contained inside of the `address` endpoint.
|
|
func (d *quicDialerResolver) DialContext(
|
|
ctx context.Context, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, 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
|
|
}
|
|
tlsConfig = d.maybeApplyTLSDefaults(tlsConfig, onlyhost)
|
|
// See TODO(https://github.com/ooni/probe/issues/1779) however
|
|
// this is less of a problem for QUIC because so far we have been
|
|
// using it to perform research only (i.e., urlgetter).
|
|
addrs = quirkSortIPAddrs(addrs)
|
|
var errorslist []error
|
|
for _, addr := range addrs {
|
|
target := net.JoinHostPort(addr, onlyport)
|
|
qconn, err := d.Dialer.DialContext(
|
|
ctx, target, tlsConfig, quicConfig)
|
|
if err == nil {
|
|
return qconn, nil
|
|
}
|
|
errorslist = append(errorslist, err)
|
|
}
|
|
return nil, quirkReduceErrors(errorslist)
|
|
}
|
|
|
|
// maybeApplyTLSDefaults sets the SNI if it's not already configured.
|
|
func (d *quicDialerResolver) maybeApplyTLSDefaults(config *tls.Config, host string) *tls.Config {
|
|
config = config.Clone()
|
|
if config.ServerName == "" {
|
|
config.ServerName = host
|
|
}
|
|
return config
|
|
}
|
|
|
|
// lookupHost performs a domain name resolution.
|
|
func (d *quicDialerResolver) 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 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 model.QUICDialer
|
|
|
|
// Logger is the underlying logger.
|
|
Logger 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.QUICDialer = &quicDialerLogger{}
|
|
|
|
// DialContext implements QUICContextDialer.DialContext.
|
|
func (d *quicDialerLogger) DialContext(
|
|
ctx context.Context, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
|
|
d.Logger.Debugf("quic_dial%s %s/udp...", d.operationSuffix, address)
|
|
qconn, err := d.Dialer.DialContext(ctx, address, tlsConfig, quicConfig)
|
|
if err != nil {
|
|
d.Logger.Debugf("quic_dial%s %s/udp... %s", d.operationSuffix,
|
|
address, err)
|
|
return nil, err
|
|
}
|
|
d.Logger.Debugf("quic_dial%s %s/udp... ok", d.operationSuffix, address)
|
|
return qconn, nil
|
|
}
|
|
|
|
// CloseIdleConnections implements QUICDialer.CloseIdleConnections.
|
|
func (d *quicDialerLogger) CloseIdleConnections() {
|
|
d.Dialer.CloseIdleConnections()
|
|
}
|
|
|
|
// NewSingleUseQUICDialer is like NewSingleUseDialer but for QUIC.
|
|
func NewSingleUseQUICDialer(qconn quic.EarlyConnection) model.QUICDialer {
|
|
return &quicDialerSingleUse{qconn: qconn}
|
|
}
|
|
|
|
// quicDialerSingleUse is the QUICDialer returned by NewSingleQUICDialer.
|
|
type quicDialerSingleUse struct {
|
|
mu sync.Mutex
|
|
qconn quic.EarlyConnection
|
|
}
|
|
|
|
var _ model.QUICDialer = &quicDialerSingleUse{}
|
|
|
|
// DialContext implements QUICDialer.DialContext.
|
|
func (s *quicDialerSingleUse) DialContext(
|
|
ctx context.Context, addr string, tlsCfg *tls.Config,
|
|
cfg *quic.Config) (quic.EarlyConnection, error) {
|
|
var qconn quic.EarlyConnection
|
|
defer s.mu.Unlock()
|
|
s.mu.Lock()
|
|
if s.qconn == nil {
|
|
return nil, ErrNoConnReuse
|
|
}
|
|
qconn, s.qconn = s.qconn, nil
|
|
return qconn, nil
|
|
}
|
|
|
|
// CloseIdleConnections closes idle connections.
|
|
func (s *quicDialerSingleUse) CloseIdleConnections() {
|
|
// nothing to do
|
|
}
|
|
|
|
// quicListenerErrWrapper is a QUICListener that wraps errors.
|
|
type quicListenerErrWrapper struct {
|
|
// QUICListener is the underlying listener.
|
|
QUICListener model.QUICListener
|
|
}
|
|
|
|
var _ model.QUICListener = &quicListenerErrWrapper{}
|
|
|
|
// Listen implements QUICListener.Listen.
|
|
func (qls *quicListenerErrWrapper) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) {
|
|
pconn, err := qls.QUICListener.Listen(addr)
|
|
if err != nil {
|
|
return nil, NewErrWrapper(ClassifyGenericError, QUICListenOperation, err)
|
|
}
|
|
return &quicErrWrapperUDPLikeConn{pconn}, nil
|
|
}
|
|
|
|
// quicErrWrapperUDPLikeConn is a UDPLikeConn that wraps errors.
|
|
type quicErrWrapperUDPLikeConn struct {
|
|
// UDPLikeConn is the underlying conn.
|
|
model.UDPLikeConn
|
|
}
|
|
|
|
var _ model.UDPLikeConn = &quicErrWrapperUDPLikeConn{}
|
|
|
|
// WriteTo implements UDPLikeConn.WriteTo.
|
|
func (c *quicErrWrapperUDPLikeConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
|
count, err := c.UDPLikeConn.WriteTo(p, addr)
|
|
if err != nil {
|
|
return 0, NewErrWrapper(ClassifyGenericError, WriteToOperation, err)
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// ReadFrom implements UDPLikeConn.ReadFrom.
|
|
func (c *quicErrWrapperUDPLikeConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
|
n, addr, err := c.UDPLikeConn.ReadFrom(b)
|
|
if err != nil {
|
|
return 0, nil, NewErrWrapper(ClassifyGenericError, ReadFromOperation, err)
|
|
}
|
|
return n, addr, nil
|
|
}
|
|
|
|
// Close implements UDPLikeConn.Close.
|
|
func (c *quicErrWrapperUDPLikeConn) Close() error {
|
|
err := c.UDPLikeConn.Close()
|
|
if err != nil {
|
|
return NewErrWrapper(ClassifyGenericError, ReadFromOperation, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// quicDialerErrWrapper is a dialer that performs quic err wrapping
|
|
type quicDialerErrWrapper struct {
|
|
QUICDialer model.QUICDialer
|
|
}
|
|
|
|
var _ model.QUICDialer = &quicDialerErrWrapper{}
|
|
|
|
// DialContext implements ContextDialer.DialContext
|
|
func (d *quicDialerErrWrapper) DialContext(
|
|
ctx context.Context, host string,
|
|
tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
|
qconn, err := d.QUICDialer.DialContext(ctx, host, tlsCfg, cfg)
|
|
if err != nil {
|
|
return nil, NewErrWrapper(
|
|
ClassifyQUICHandshakeError, QUICHandshakeOperation, err)
|
|
}
|
|
return qconn, nil
|
|
}
|
|
|
|
func (d *quicDialerErrWrapper) CloseIdleConnections() {
|
|
d.QUICDialer.CloseIdleConnections()
|
|
}
|