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