github.com/metacubex/mihomo@v1.18.5/dns/doh.go (about)

     1  package dns
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"encoding/base64"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"net/http"
    12  	"net/url"
    13  	"runtime"
    14  	"strconv"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/metacubex/mihomo/component/ca"
    19  	C "github.com/metacubex/mihomo/constant"
    20  	"github.com/metacubex/mihomo/log"
    21  	"github.com/metacubex/quic-go"
    22  	"github.com/metacubex/quic-go/http3"
    23  	D "github.com/miekg/dns"
    24  	"golang.org/x/exp/slices"
    25  	"golang.org/x/net/http2"
    26  )
    27  
    28  // Values to configure HTTP and HTTP/2 transport.
    29  const (
    30  	// transportDefaultReadIdleTimeout is the default timeout for pinging
    31  	// idle connections in HTTP/2 transport.
    32  	transportDefaultReadIdleTimeout = 30 * time.Second
    33  
    34  	// transportDefaultIdleConnTimeout is the default timeout for idle
    35  	// connections in HTTP transport.
    36  	transportDefaultIdleConnTimeout = 5 * time.Minute
    37  
    38  	// dohMaxConnsPerHost controls the maximum number of connections for
    39  	// each host.
    40  	dohMaxConnsPerHost = 1
    41  	dialTimeout        = 10 * time.Second
    42  
    43  	// dohMaxIdleConns controls the maximum number of connections being idle
    44  	// at the same time.
    45  	dohMaxIdleConns = 1
    46  	maxElapsedTime  = time.Second * 30
    47  )
    48  
    49  var DefaultHTTPVersions = []C.HTTPVersion{C.HTTPVersion11, C.HTTPVersion2}
    50  
    51  // dnsOverHTTPS is a struct that implements the Upstream interface for the
    52  // DNS-over-HTTPS protocol.
    53  type dnsOverHTTPS struct {
    54  	// The Client's Transport typically has internal state (cached TCP
    55  	// connections), so Clients should be reused instead of created as
    56  	// needed. Clients are safe for concurrent use by multiple goroutines.
    57  	client   *http.Client
    58  	clientMu sync.Mutex
    59  
    60  	// quicConfig is the QUIC configuration that is used if HTTP/3 is enabled
    61  	// for this upstream.
    62  	quicConfig      *quic.Config
    63  	quicConfigGuard sync.Mutex
    64  	url             *url.URL
    65  	r               *Resolver
    66  	httpVersions    []C.HTTPVersion
    67  	proxyAdapter    C.ProxyAdapter
    68  	proxyName       string
    69  	addr            string
    70  }
    71  
    72  // type check
    73  var _ dnsClient = (*dnsOverHTTPS)(nil)
    74  
    75  // newDoH returns the DNS-over-HTTPS Upstream.
    76  func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) dnsClient {
    77  	u, _ := url.Parse(urlString)
    78  	httpVersions := DefaultHTTPVersions
    79  	if preferH3 {
    80  		httpVersions = append(httpVersions, C.HTTPVersion3)
    81  	}
    82  
    83  	if params["h3"] == "true" {
    84  		httpVersions = []C.HTTPVersion{C.HTTPVersion3}
    85  	}
    86  
    87  	doh := &dnsOverHTTPS{
    88  		url:          u,
    89  		addr:         u.String(),
    90  		r:            r,
    91  		proxyAdapter: proxyAdapter,
    92  		proxyName:    proxyName,
    93  		quicConfig: &quic.Config{
    94  			KeepAlivePeriod: QUICKeepAlivePeriod,
    95  			TokenStore:      newQUICTokenStore(),
    96  		},
    97  		httpVersions: httpVersions,
    98  	}
    99  
   100  	runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close)
   101  
   102  	return doh
   103  }
   104  
   105  // Address implements the Upstream interface for *dnsOverHTTPS.
   106  func (doh *dnsOverHTTPS) Address() string {
   107  	return doh.addr
   108  }
   109  func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
   110  	// Quote from https://www.rfc-editor.org/rfc/rfc8484.html:
   111  	// In order to maximize HTTP cache friendliness, DoH clients using media
   112  	// formats that include the ID field from the DNS message header, such
   113  	// as "application/dns-message", SHOULD use a DNS ID of 0 in every DNS
   114  	// request.
   115  	m = m.Copy()
   116  	id := m.Id
   117  	m.Id = 0
   118  	defer func() {
   119  		// Restore the original ID to not break compatibility with proxies.
   120  		m.Id = id
   121  		if msg != nil {
   122  			msg.Id = id
   123  		}
   124  	}()
   125  
   126  	// Check if there was already an active client before sending the request.
   127  	// We'll only attempt to re-connect if there was one.
   128  	client, isCached, err := doh.getClient(ctx)
   129  	if err != nil {
   130  		return nil, fmt.Errorf("failed to init http client: %w", err)
   131  	}
   132  
   133  	// Make the first attempt to send the DNS query.
   134  	msg, err = doh.exchangeHTTPS(ctx, client, m)
   135  
   136  	// Make up to 2 attempts to re-create the HTTP client and send the request
   137  	// again.  There are several cases (mostly, with QUIC) where this workaround
   138  	// is necessary to make HTTP client usable.  We need to make 2 attempts in
   139  	// the case when the connection was closed (due to inactivity for example)
   140  	// AND the server refuses to open a 0-RTT connection.
   141  	for i := 0; isCached && doh.shouldRetry(err) && i < 2; i++ {
   142  		client, err = doh.resetClient(ctx, err)
   143  		if err != nil {
   144  			return nil, fmt.Errorf("failed to reset http client: %w", err)
   145  		}
   146  
   147  		msg, err = doh.exchangeHTTPS(ctx, client, m)
   148  	}
   149  
   150  	if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
   151  		// If the request failed anyway, make sure we don't use this client.
   152  		_, resErr := doh.resetClient(ctx, err)
   153  
   154  		return nil, fmt.Errorf("%w (resErr:%v)", err, resErr)
   155  	}
   156  
   157  	return msg, err
   158  }
   159  
   160  // Close implements the Upstream interface for *dnsOverHTTPS.
   161  func (doh *dnsOverHTTPS) Close() (err error) {
   162  	doh.clientMu.Lock()
   163  	defer doh.clientMu.Unlock()
   164  
   165  	runtime.SetFinalizer(doh, nil)
   166  
   167  	if doh.client == nil {
   168  		return nil
   169  	}
   170  
   171  	return doh.closeClient(doh.client)
   172  }
   173  
   174  // closeClient cleans up resources used by client if necessary.  Note, that at
   175  // this point it should only be done for HTTP/3 as it may leak due to keep-alive
   176  // connections.
   177  func (doh *dnsOverHTTPS) closeClient(client *http.Client) (err error) {
   178  	if isHTTP3(client) {
   179  		return client.Transport.(io.Closer).Close()
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  // exchangeHTTPS logs the request and its result and calls exchangeHTTPSClient.
   186  func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) {
   187  	resp, err = doh.exchangeHTTPSClient(ctx, client, req)
   188  	return resp, err
   189  }
   190  
   191  // exchangeHTTPSClient sends the DNS query to a DoH resolver using the specified
   192  // http.Client instance.
   193  func (doh *dnsOverHTTPS) exchangeHTTPSClient(
   194  	ctx context.Context,
   195  	client *http.Client,
   196  	req *D.Msg,
   197  ) (resp *D.Msg, err error) {
   198  	buf, err := req.Pack()
   199  	if err != nil {
   200  		return nil, fmt.Errorf("packing message: %w", err)
   201  	}
   202  
   203  	// It appears, that GET requests are more memory-efficient with Golang
   204  	// implementation of HTTP/2.
   205  	method := http.MethodGet
   206  	if isHTTP3(client) {
   207  		// If we're using HTTP/3, use http3.MethodGet0RTT to force using 0-RTT.
   208  		method = http3.MethodGet0RTT
   209  	}
   210  
   211  	url := doh.url
   212  	url.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf))
   213  	httpReq, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   214  	if err != nil {
   215  		return nil, fmt.Errorf("creating http request to %s: %w", url, err)
   216  	}
   217  
   218  	httpReq.Header.Set("Accept", "application/dns-message")
   219  	httpReq.Header.Set("User-Agent", "")
   220  	httpResp, err := client.Do(httpReq)
   221  	if err != nil {
   222  		return nil, fmt.Errorf("requesting %s: %w", url, err)
   223  	}
   224  	defer httpResp.Body.Close()
   225  
   226  	body, err := io.ReadAll(httpResp.Body)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("reading %s: %w", url, err)
   229  	}
   230  
   231  	if httpResp.StatusCode != http.StatusOK {
   232  		return nil,
   233  			fmt.Errorf(
   234  				"expected status %d, got %d from %s",
   235  				http.StatusOK,
   236  				httpResp.StatusCode,
   237  				url,
   238  			)
   239  	}
   240  
   241  	resp = &D.Msg{}
   242  	err = resp.Unpack(body)
   243  	if err != nil {
   244  		return nil, fmt.Errorf(
   245  			"unpacking response from %s: body is %s: %w",
   246  			url,
   247  			body,
   248  			err,
   249  		)
   250  	}
   251  
   252  	if resp.Id != req.Id {
   253  		err = D.ErrId
   254  	}
   255  
   256  	return resp, err
   257  }
   258  
   259  // shouldRetry checks what error we have received and returns true if we should
   260  // re-create the HTTP client and retry the request.
   261  func (doh *dnsOverHTTPS) shouldRetry(err error) (ok bool) {
   262  	if err == nil {
   263  		return false
   264  	}
   265  
   266  	var netErr net.Error
   267  	if errors.As(err, &netErr) && netErr.Timeout() {
   268  		// If this is a timeout error, trying to forcibly re-create the HTTP
   269  		// client instance.  This is an attempt to fix an issue with DoH client
   270  		// stalling after a network change.
   271  		//
   272  		// See https://github.com/AdguardTeam/AdGuardHome/issues/3217.
   273  		return true
   274  	}
   275  
   276  	if isQUICRetryError(err) {
   277  		return true
   278  	}
   279  
   280  	return false
   281  }
   282  
   283  // resetClient triggers re-creation of the *http.Client that is used by this
   284  // upstream.  This method accepts the error that caused resetting client as
   285  // depending on the error we may also reset the QUIC config.
   286  func (doh *dnsOverHTTPS) resetClient(ctx context.Context, resetErr error) (client *http.Client, err error) {
   287  	doh.clientMu.Lock()
   288  	defer doh.clientMu.Unlock()
   289  
   290  	if errors.Is(resetErr, quic.Err0RTTRejected) {
   291  		// Reset the TokenStore only if 0-RTT was rejected.
   292  		doh.resetQUICConfig()
   293  	}
   294  
   295  	oldClient := doh.client
   296  	if oldClient != nil {
   297  		closeErr := doh.closeClient(oldClient)
   298  		if closeErr != nil {
   299  			log.Warnln("warning: failed to close the old http client: %v", closeErr)
   300  		}
   301  	}
   302  
   303  	log.Debugln("re-creating the http client due to %v", resetErr)
   304  	doh.client, err = doh.createClient(ctx)
   305  
   306  	return doh.client, err
   307  }
   308  
   309  // getQUICConfig returns the QUIC config in a thread-safe manner.  Note, that
   310  // this method returns a pointer, it is forbidden to change its properties.
   311  func (doh *dnsOverHTTPS) getQUICConfig() (c *quic.Config) {
   312  	doh.quicConfigGuard.Lock()
   313  	defer doh.quicConfigGuard.Unlock()
   314  
   315  	return doh.quicConfig
   316  }
   317  
   318  // resetQUICConfig Re-create the token store to make sure we're not trying to
   319  // use invalid for 0-RTT.
   320  func (doh *dnsOverHTTPS) resetQUICConfig() {
   321  	doh.quicConfigGuard.Lock()
   322  	defer doh.quicConfigGuard.Unlock()
   323  
   324  	doh.quicConfig = doh.quicConfig.Clone()
   325  	doh.quicConfig.TokenStore = newQUICTokenStore()
   326  }
   327  
   328  // getClient gets or lazily initializes an HTTP client (and transport) that will
   329  // be used for this DoH resolver.
   330  func (doh *dnsOverHTTPS) getClient(ctx context.Context) (c *http.Client, isCached bool, err error) {
   331  	startTime := time.Now()
   332  
   333  	doh.clientMu.Lock()
   334  	defer doh.clientMu.Unlock()
   335  	if doh.client != nil {
   336  		return doh.client, true, nil
   337  	}
   338  
   339  	// Timeout can be exceeded while waiting for the lock. This happens quite
   340  	// often on mobile devices.
   341  	elapsed := time.Since(startTime)
   342  	if elapsed > maxElapsedTime {
   343  		return nil, false, fmt.Errorf("timeout exceeded: %s", elapsed)
   344  	}
   345  
   346  	log.Debugln("creating a new http client")
   347  	doh.client, err = doh.createClient(ctx)
   348  
   349  	return doh.client, false, err
   350  }
   351  
   352  // createClient creates a new *http.Client instance.  The HTTP protocol version
   353  // will depend on whether HTTP3 is allowed and provided by this upstream.  Note,
   354  // that we'll attempt to establish a QUIC connection when creating the client in
   355  // order to check whether HTTP3 is supported.
   356  func (doh *dnsOverHTTPS) createClient(ctx context.Context) (*http.Client, error) {
   357  	transport, err := doh.createTransport(ctx)
   358  	if err != nil {
   359  		return nil, fmt.Errorf("[%s] initializing http transport: %w", doh.url.String(), err)
   360  	}
   361  
   362  	client := &http.Client{
   363  		Transport: transport,
   364  		Timeout:   DefaultTimeout,
   365  		Jar:       nil,
   366  	}
   367  
   368  	doh.client = client
   369  
   370  	return doh.client, nil
   371  }
   372  
   373  // createTransport initializes an HTTP transport that will be used specifically
   374  // for this DoH resolver.  This HTTP transport ensures that the HTTP requests
   375  // will be sent exactly to the IP address got from the bootstrap resolver. Note,
   376  // that this function will first attempt to establish a QUIC connection (if
   377  // HTTP3 is enabled in the upstream options).  If this attempt is successful,
   378  // it returns an HTTP3 transport, otherwise it returns the H1/H2 transport.
   379  func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripper, err error) {
   380  	tlsConfig := ca.GetGlobalTLSConfig(
   381  		&tls.Config{
   382  			InsecureSkipVerify:     false,
   383  			MinVersion:             tls.VersionTLS12,
   384  			SessionTicketsDisabled: false,
   385  		})
   386  	var nextProtos []string
   387  	for _, v := range doh.httpVersions {
   388  		nextProtos = append(nextProtos, string(v))
   389  	}
   390  	tlsConfig.NextProtos = nextProtos
   391  	dialContext := getDialHandler(doh.r, doh.proxyAdapter, doh.proxyName)
   392  
   393  	if slices.Contains(doh.httpVersions, C.HTTPVersion3) {
   394  		// First, we attempt to create an HTTP3 transport.  If the probe QUIC
   395  		// connection is established successfully, we'll be using HTTP3 for this
   396  		// upstream.
   397  		transportH3, err := doh.createTransportH3(ctx, tlsConfig, dialContext)
   398  		if err == nil {
   399  			log.Debugln("[%s] using HTTP/3 for this upstream: QUIC was faster", doh.url.String())
   400  			return transportH3, nil
   401  		}
   402  	}
   403  
   404  	log.Debugln("[%s] using HTTP/2 for this upstream: %v", doh.url.String(), err)
   405  
   406  	if !doh.supportsHTTP() {
   407  		return nil, errors.New("HTTP1/1 and HTTP2 are not supported by this upstream")
   408  	}
   409  
   410  	transport := &http.Transport{
   411  		TLSClientConfig:    tlsConfig,
   412  		DisableCompression: true,
   413  		DialContext:        dialContext,
   414  		IdleConnTimeout:    transportDefaultIdleConnTimeout,
   415  		MaxConnsPerHost:    dohMaxConnsPerHost,
   416  		MaxIdleConns:       dohMaxIdleConns,
   417  		// Since we have a custom DialContext, we need to use this field to
   418  		// make golang http.Client attempt to use HTTP/2. Otherwise, it would
   419  		// only be used when negotiated on the TLS level.
   420  		ForceAttemptHTTP2: true,
   421  	}
   422  
   423  	// Explicitly configure transport to use HTTP/2.
   424  	//
   425  	// See https://github.com/AdguardTeam/dnsproxy/issues/11.
   426  	var transportH2 *http2.Transport
   427  	transportH2, err = http2.ConfigureTransports(transport)
   428  	if err != nil {
   429  		return nil, err
   430  	}
   431  
   432  	// Enable HTTP/2 pings on idle connections.
   433  	transportH2.ReadIdleTimeout = transportDefaultReadIdleTimeout
   434  
   435  	return transport, nil
   436  }
   437  
   438  // http3Transport is a wrapper over *http3.RoundTripper that tries to optimize
   439  // its behavior.  The main thing that it does is trying to force use a single
   440  // connection to a host instead of creating a new one all the time.  It also
   441  // helps mitigate race issues with quic-go.
   442  type http3Transport struct {
   443  	baseTransport *http3.RoundTripper
   444  
   445  	closed bool
   446  	mu     sync.RWMutex
   447  }
   448  
   449  // type check
   450  var _ http.RoundTripper = (*http3Transport)(nil)
   451  
   452  // RoundTrip implements the http.RoundTripper interface for *http3Transport.
   453  func (h *http3Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
   454  	h.mu.RLock()
   455  	defer h.mu.RUnlock()
   456  
   457  	if h.closed {
   458  		return nil, net.ErrClosed
   459  	}
   460  
   461  	// Try to use cached connection to the target host if it's available.
   462  	resp, err = h.baseTransport.RoundTripOpt(req, http3.RoundTripOpt{OnlyCachedConn: true})
   463  
   464  	if errors.Is(err, http3.ErrNoCachedConn) {
   465  		// If there are no cached connection, trigger creating a new one.
   466  		resp, err = h.baseTransport.RoundTrip(req)
   467  	}
   468  
   469  	return resp, err
   470  }
   471  
   472  // type check
   473  var _ io.Closer = (*http3Transport)(nil)
   474  
   475  // Close implements the io.Closer interface for *http3Transport.
   476  func (h *http3Transport) Close() (err error) {
   477  	h.mu.Lock()
   478  	defer h.mu.Unlock()
   479  
   480  	h.closed = true
   481  
   482  	return h.baseTransport.Close()
   483  }
   484  
   485  // createTransportH3 tries to create an HTTP/3 transport for this upstream.
   486  // We should be able to fall back to H1/H2 in case if HTTP/3 is unavailable or
   487  // if it is too slow.  In order to do that, this method will run two probes
   488  // in parallel (one for TLS, the other one for QUIC) and if QUIC is faster it
   489  // will create the *http3.RoundTripper instance.
   490  func (doh *dnsOverHTTPS) createTransportH3(
   491  	ctx context.Context,
   492  	tlsConfig *tls.Config,
   493  	dialContext dialHandler,
   494  ) (roundTripper http.RoundTripper, err error) {
   495  	if !doh.supportsH3() {
   496  		return nil, errors.New("HTTP3 support is not enabled")
   497  	}
   498  
   499  	addr, err := doh.probeH3(ctx, tlsConfig, dialContext)
   500  	if err != nil {
   501  		return nil, err
   502  	}
   503  
   504  	rt := &http3.RoundTripper{
   505  		Dial: func(
   506  			ctx context.Context,
   507  
   508  			// Ignore the address and always connect to the one that we got
   509  			// from the bootstrapper.
   510  			_ string,
   511  			tlsCfg *tls.Config,
   512  			cfg *quic.Config,
   513  		) (c quic.EarlyConnection, err error) {
   514  			return doh.dialQuic(ctx, addr, tlsCfg, cfg)
   515  		},
   516  		DisableCompression: true,
   517  		TLSClientConfig:    tlsConfig,
   518  		QUICConfig:         doh.getQUICConfig(),
   519  	}
   520  
   521  	return &http3Transport{baseTransport: rt}, nil
   522  }
   523  
   524  func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
   525  	ip, port, err := net.SplitHostPort(addr)
   526  	if err != nil {
   527  		return nil, err
   528  	}
   529  	portInt, err := strconv.Atoi(port)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  	udpAddr := net.UDPAddr{
   534  		IP:   net.ParseIP(ip),
   535  		Port: portInt,
   536  	}
   537  	conn, err := listenPacket(ctx, doh.proxyAdapter, doh.proxyName, "udp", addr, doh.r)
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  	transport := quic.Transport{Conn: conn}
   542  	transport.SetCreatedConn(true) // auto close conn
   543  	transport.SetSingleUse(true)   // auto close transport
   544  	tlsCfg = tlsCfg.Clone()
   545  	if host, _, err := net.SplitHostPort(doh.url.Host); err == nil {
   546  		tlsCfg.ServerName = host
   547  	} else {
   548  		// It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port.
   549  		tlsCfg.ServerName = doh.url.Host
   550  	}
   551  	return transport.DialEarly(ctx, &udpAddr, tlsCfg, cfg)
   552  }
   553  
   554  // probeH3 runs a test to check whether QUIC is faster than TLS for this
   555  // upstream.  If the test is successful it will return the address that we
   556  // should use to establish the QUIC connections.
   557  func (doh *dnsOverHTTPS) probeH3(
   558  	ctx context.Context,
   559  	tlsConfig *tls.Config,
   560  	dialContext dialHandler,
   561  ) (addr string, err error) {
   562  	// We're using bootstrapped address instead of what's passed to the function
   563  	// it does not create an actual connection, but it helps us determine
   564  	// what IP is actually reachable (when there are v4/v6 addresses).
   565  	rawConn, err := dialContext(ctx, "udp", doh.url.Host)
   566  	if err != nil {
   567  		return "", fmt.Errorf("failed to dial: %w", err)
   568  	}
   569  	addr = rawConn.RemoteAddr().String()
   570  	// It's never actually used.
   571  	_ = rawConn.Close()
   572  
   573  	// Avoid spending time on probing if this upstream only supports HTTP/3.
   574  	if doh.supportsH3() && !doh.supportsHTTP() {
   575  		return addr, nil
   576  	}
   577  
   578  	// Use a new *tls.Config with empty session cache for probe connections.
   579  	// Surprisingly, this is really important since otherwise it invalidates
   580  	// the existing cache.
   581  	// TODO(ameshkov): figure out why the sessions cache invalidates here.
   582  	probeTLSCfg := tlsConfig.Clone()
   583  	probeTLSCfg.ClientSessionCache = nil
   584  
   585  	// Do not expose probe connections to the callbacks that are passed to
   586  	// the bootstrap options to avoid side-effects.
   587  	// TODO(ameshkov): consider exposing, somehow mark that this is a probe.
   588  	probeTLSCfg.VerifyPeerCertificate = nil
   589  	probeTLSCfg.VerifyConnection = nil
   590  
   591  	// Run probeQUIC and probeTLS in parallel and see which one is faster.
   592  	chQuic := make(chan error, 1)
   593  	chTLS := make(chan error, 1)
   594  	go doh.probeQUIC(ctx, addr, probeTLSCfg, chQuic)
   595  	go doh.probeTLS(ctx, dialContext, probeTLSCfg, chTLS)
   596  
   597  	select {
   598  	case quicErr := <-chQuic:
   599  		if quicErr != nil {
   600  			// QUIC failed, return error since HTTP3 was not preferred.
   601  			return "", quicErr
   602  		}
   603  
   604  		// Return immediately, QUIC was faster.
   605  		return addr, quicErr
   606  	case tlsErr := <-chTLS:
   607  		if tlsErr != nil {
   608  			// Return immediately, TLS failed.
   609  			log.Debugln("probing TLS: %v", tlsErr)
   610  			return addr, nil
   611  		}
   612  
   613  		return "", errors.New("TLS was faster than QUIC, prefer it")
   614  	}
   615  }
   616  
   617  // probeQUIC attempts to establish a QUIC connection to the specified address.
   618  // We run probeQUIC and probeTLS in parallel and see which one is faster.
   619  func (doh *dnsOverHTTPS) probeQUIC(ctx context.Context, addr string, tlsConfig *tls.Config, ch chan error) {
   620  	startTime := time.Now()
   621  	conn, err := doh.dialQuic(ctx, addr, tlsConfig, doh.getQUICConfig())
   622  	if err != nil {
   623  		ch <- fmt.Errorf("opening QUIC connection to %s: %w", doh.Address(), err)
   624  		return
   625  	}
   626  
   627  	// Ignore the error since there's no way we can use it for anything useful.
   628  	_ = conn.CloseWithError(QUICCodeNoError, "")
   629  
   630  	ch <- nil
   631  
   632  	elapsed := time.Now().Sub(startTime)
   633  	log.Debugln("elapsed on establishing a QUIC connection: %s", elapsed)
   634  }
   635  
   636  // probeTLS attempts to establish a TLS connection to the specified address. We
   637  // run probeQUIC and probeTLS in parallel and see which one is faster.
   638  func (doh *dnsOverHTTPS) probeTLS(ctx context.Context, dialContext dialHandler, tlsConfig *tls.Config, ch chan error) {
   639  	startTime := time.Now()
   640  
   641  	conn, err := doh.tlsDial(ctx, dialContext, "tcp", tlsConfig)
   642  	if err != nil {
   643  		ch <- fmt.Errorf("opening TLS connection: %w", err)
   644  		return
   645  	}
   646  
   647  	// Ignore the error since there's no way we can use it for anything useful.
   648  	_ = conn.Close()
   649  
   650  	ch <- nil
   651  
   652  	elapsed := time.Now().Sub(startTime)
   653  	log.Debugln("elapsed on establishing a TLS connection: %s", elapsed)
   654  }
   655  
   656  // supportsH3 returns true if HTTP/3 is supported by this upstream.
   657  func (doh *dnsOverHTTPS) supportsH3() (ok bool) {
   658  	for _, v := range doh.supportedHTTPVersions() {
   659  		if v == C.HTTPVersion3 {
   660  			return true
   661  		}
   662  	}
   663  
   664  	return false
   665  }
   666  
   667  // supportsHTTP returns true if HTTP/1.1 or HTTP2 is supported by this upstream.
   668  func (doh *dnsOverHTTPS) supportsHTTP() (ok bool) {
   669  	for _, v := range doh.supportedHTTPVersions() {
   670  		if v == C.HTTPVersion11 || v == C.HTTPVersion2 {
   671  			return true
   672  		}
   673  	}
   674  
   675  	return false
   676  }
   677  
   678  // supportedHTTPVersions returns the list of supported HTTP versions.
   679  func (doh *dnsOverHTTPS) supportedHTTPVersions() (v []C.HTTPVersion) {
   680  	v = doh.httpVersions
   681  	if v == nil {
   682  		v = DefaultHTTPVersions
   683  	}
   684  
   685  	return v
   686  }
   687  
   688  // isHTTP3 checks if the *http.Client is an HTTP/3 client.
   689  func isHTTP3(client *http.Client) (ok bool) {
   690  	_, ok = client.Transport.(*http3Transport)
   691  
   692  	return ok
   693  }
   694  
   695  // tlsDial is basically the same as tls.DialWithDialer, but we will call our own
   696  // dialContext function to get connection.
   697  func (doh *dnsOverHTTPS) tlsDial(ctx context.Context, dialContext dialHandler, network string, config *tls.Config) (*tls.Conn, error) {
   698  	// We're using bootstrapped address instead of what's passed
   699  	// to the function.
   700  	rawConn, err := dialContext(ctx, network, doh.url.Host)
   701  	if err != nil {
   702  		return nil, err
   703  	}
   704  
   705  	// We want the timeout to cover the whole process: TCP connection and
   706  	// TLS handshake dialTimeout will be used as connection deadLine.
   707  	conn := tls.Client(rawConn, config)
   708  
   709  	err = conn.SetDeadline(time.Now().Add(dialTimeout))
   710  	if err != nil {
   711  		// Must not happen in normal circumstances.
   712  		log.Errorln("cannot set deadline: %v", err)
   713  		return nil, err
   714  	}
   715  
   716  	err = conn.Handshake()
   717  	if err != nil {
   718  		defer conn.Close()
   719  		return nil, err
   720  	}
   721  
   722  	return conn, nil
   723  }