ooni-probe-cli/internal/netxlite/quic.go
Simone Basso 86ffd6a0c4
feat: reintroduce the tproxy functionality (#975)
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.
2022-10-12 17:38:33 +02:00

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()
}