250a595f89
* refactor: cleaner way of passing a UDPConn around Also part of https://github.com/ooni/probe/issues/1505 * Update internal/engine/netx/quicdialer/connectionstate.go
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
package netxlite
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"strconv"
|
|
|
|
"github.com/lucas-clemente/quic-go"
|
|
"github.com/ooni/probe-cli/v3/internal/quicx"
|
|
)
|
|
|
|
// QUICContextDialer is a dialer for QUIC using Context.
|
|
type QUICContextDialer interface {
|
|
// DialContext establishes a new QUIC session using the given
|
|
// network and address. The tlsConfig and the quicConfig arguments
|
|
// MUST NOT be nil. Returns either the session or an error.
|
|
DialContext(ctx context.Context, network, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error)
|
|
}
|
|
|
|
// QUICListener listens for QUIC connections.
|
|
type QUICListener interface {
|
|
// Listen creates a new listening UDPConn.
|
|
Listen(addr *net.UDPAddr) (quicx.UDPConn, error)
|
|
}
|
|
|
|
// QUICListenerStdlib is a QUICListener using the standard library.
|
|
type QUICListenerStdlib struct{}
|
|
|
|
var _ QUICListener = &QUICListenerStdlib{}
|
|
|
|
// Listen implements QUICListener.Listen.
|
|
func (qls *QUICListenerStdlib) Listen(addr *net.UDPAddr) (quicx.UDPConn, error) {
|
|
return net.ListenUDP("udp", addr)
|
|
}
|
|
|
|
// QUICDialerQUICGo dials using the lucas-clemente/quic-go library.
|
|
type QUICDialerQUICGo struct {
|
|
// QUICListener is the underlying QUICListener to use.
|
|
QUICListener 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.EarlySession, error)
|
|
}
|
|
|
|
var _ QUICContextDialer = &QUICDialerQUICGo{}
|
|
|
|
// errInvalidIP indicates that a string is not a valid IP.
|
|
var errInvalidIP = errors.New("netxlite: invalid IP")
|
|
|
|
// DialContext implements ContextDialer.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, network string,
|
|
address string, tlsConfig *tls.Config, quicConfig *quic.Config) (
|
|
quic.EarlySession, error) {
|
|
onlyhost, onlyport, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port, err := strconv.Atoi(onlyport)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ip := net.ParseIP(onlyhost)
|
|
if ip == nil {
|
|
return nil, errInvalidIP
|
|
}
|
|
pconn, err := d.QUICListener.Listen(&net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
udpAddr := &net.UDPAddr{IP: ip, Port: port, Zone: ""}
|
|
tlsConfig = d.maybeApplyTLSDefaults(tlsConfig, port)
|
|
sess, err := d.dialEarlyContext(
|
|
ctx, pconn, udpAddr, address, tlsConfig, quicConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &quicSessionOwnsConn{EarlySession: sess, conn: pconn}, nil
|
|
}
|
|
|
|
func (d *QUICDialerQUICGo) dialEarlyContext(ctx context.Context,
|
|
pconn net.PacketConn, remoteAddr net.Addr, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, 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
|
|
}
|
|
|
|
// quicSessionOwnsConn ensures that we close the PacketConn.
|
|
type quicSessionOwnsConn struct {
|
|
// EarlySession is the embedded early session
|
|
quic.EarlySession
|
|
|
|
// conn is the connection we own
|
|
conn net.PacketConn
|
|
}
|
|
|
|
// CloseWithError implements quic.EarlySession.CloseWithError.
|
|
func (sess *quicSessionOwnsConn) CloseWithError(
|
|
code quic.ApplicationErrorCode, reason string) error {
|
|
err := sess.EarlySession.CloseWithError(code, reason)
|
|
sess.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 QUIC dialer.
|
|
Dialer QUICContextDialer
|
|
|
|
// Resolver is the underlying resolver.
|
|
Resolver Resolver
|
|
}
|
|
|
|
var _ QUICContextDialer = &QUICDialerResolver{}
|
|
|
|
// DialContext implements QUICContextDialer.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, network, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, 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)
|
|
// 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.
|
|
var errorslist []error
|
|
for _, addr := range addrs {
|
|
target := net.JoinHostPort(addr, onlyport)
|
|
sess, err := d.Dialer.DialContext(
|
|
ctx, network, target, tlsConfig, quicConfig)
|
|
if err == nil {
|
|
return sess, nil
|
|
}
|
|
errorslist = append(errorslist, err)
|
|
}
|
|
return nil, reduceErrors(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)
|
|
}
|
|
|
|
// QUICDialerLogger is a dialer with logging.
|
|
type QUICDialerLogger struct {
|
|
// Dialer is the underlying QUIC dialer.
|
|
Dialer QUICContextDialer
|
|
|
|
// Logger is the underlying logger.
|
|
Logger Logger
|
|
}
|
|
|
|
var _ QUICContextDialer = &QUICDialerLogger{}
|
|
|
|
// DialContext implements QUICContextDialer.DialContext.
|
|
func (d *QUICDialerLogger) DialContext(
|
|
ctx context.Context, network, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
|
|
d.Logger.Debugf("quic %s/%s...", address, network)
|
|
sess, err := d.Dialer.DialContext(ctx, network, address, tlsConfig, quicConfig)
|
|
if err != nil {
|
|
d.Logger.Debugf("quic %s/%s... %s", address, network, err)
|
|
return nil, err
|
|
}
|
|
d.Logger.Debugf("quic %s/%s... ok", address, network)
|
|
return sess, nil
|
|
}
|