From a03a56c89026753989a4b06787ef1a08dfbabb9c Mon Sep 17 00:00:00 2001 From: Andrey Meshkov <am@adguard.com> Date: Tue, 20 Sep 2022 15:38:01 +0300 Subject: [PATCH] Pull request: proxy: added HTTP/3 support to the DNS-over-HTTPS server implementation Merge in DNS/dnsproxy from doh3server to master Squashed commit of the following: commit dd7f6ecb0264afd16ee6fcd47ff7bafe06797645 Author: Andrey Meshkov <am@adguard.com> Date: Tue Sep 20 14:17:51 2022 +0300 upstream: fix review comments commit 3b887f614163f4900f75807c990ad2a5d354d3b5 Author: Andrey Meshkov <am@adguard.com> Date: Tue Sep 20 00:14:19 2022 +0300 proxy: added address validation logic commit b29dc3c3b6746ad5be921941904f16ab228b1dab Author: Andrey Meshkov <am@adguard.com> Date: Mon Sep 19 23:31:21 2022 +0300 proxy: fix review comments, general improvements commit 79f47f54adcd30a68a9f7bc0111025ae0a32d99d Author: Andrey Meshkov <am@adguard.com> Date: Mon Sep 19 20:43:26 2022 +0300 upstream: several improvements in DoH3 and DoQ upstreams The previous implementation weren't able to properly handle a situation when the server was restarted. This commit greatly improves the overall stability. commit 59cf92b6097d78acf6f088057134888993f7ca43 Author: Andrey Meshkov <am@adguard.com> Date: Sat Sep 17 02:51:40 2022 +0300 proxy: remoteAddr for DoH depends on HTTP version now commit 804ddedd2807870b7d36dae5ce9857de3a7f7286 Author: Andrey Meshkov <am@adguard.com> Date: Sat Sep 17 01:53:32 2022 +0300 proxy: added HTTP/3 support to the DNS-over-HTTPS server implementation The implementation follows the old approach that was used in dnsproxy, i.e. it adds another bunch of "listeners", the new ones are for HTTP/3. HTTP/3 support is not enabled by default, it should be enabled explicitly by setting HTTP3 field of proxy.Config to true. The "--http3" command-line argument now controls DoH3 support on both the client-side and the server-side. There's one more important change that was made while refactoring the code. Previously, we were creating a separate http.Server instance for every listen address that's used. It is unclear to me what's the reason for that since a single instance can be used to serve on every address. This mistake is fixed now. --- README.md | 5 + fastip/fastest.go | 3 +- fastip/ping_test.go | 6 +- go.mod | 1 + go.sum | 2 + main.go | 4 +- proxy/config.go | 1 + proxy/proxy.go | 107 ++++----- proxy/proxy_test.go | 8 +- proxy/server.go | 9 +- proxy/server_dnscrypt.go | 23 +- proxy/server_https.go | 130 +++++++---- proxy/server_https_test.go | 194 +++++++++++----- proxy/server_quic.go | 123 +++++++++-- proxy/server_quic_test.go | 2 +- upstream/bootstrap.go | 13 ++ upstream/upstream_doh.go | 120 ++++++++-- upstream/upstream_doh_test.go | 103 ++++++++- upstream/upstream_quic.go | 207 ++++++++++++------ upstream/upstream_quic_test.go | 164 +++++++++++++- vendor/github.com/bluele/gcache/LICENSE | Bin 0 -> 1077 bytes vendor/github.com/bluele/gcache/README.md | Bin 0 -> 5285 bytes vendor/github.com/bluele/gcache/arc.go | Bin 0 -> 9165 bytes vendor/github.com/bluele/gcache/cache.go | Bin 0 -> 5189 bytes vendor/github.com/bluele/gcache/clock.go | Bin 0 -> 800 bytes vendor/github.com/bluele/gcache/lfu.go | Bin 0 -> 8171 bytes vendor/github.com/bluele/gcache/lru.go | Bin 0 -> 6631 bytes vendor/github.com/bluele/gcache/simple.go | Bin 0 -> 6402 bytes .../github.com/bluele/gcache/singleflight.go | Bin 0 -> 2117 bytes vendor/github.com/bluele/gcache/stats.go | Bin 0 -> 1003 bytes vendor/github.com/bluele/gcache/utils.go | Bin 0 -> 149 bytes vendor/modules.txt | Bin 6580 -> 6660 bytes 32 files changed, 931 insertions(+), 294 deletions(-) create mode 100644 vendor/github.com/bluele/gcache/LICENSE create mode 100644 vendor/github.com/bluele/gcache/README.md create mode 100644 vendor/github.com/bluele/gcache/arc.go create mode 100644 vendor/github.com/bluele/gcache/cache.go create mode 100644 vendor/github.com/bluele/gcache/clock.go create mode 100644 vendor/github.com/bluele/gcache/lfu.go create mode 100644 vendor/github.com/bluele/gcache/lru.go create mode 100644 vendor/github.com/bluele/gcache/simple.go create mode 100644 vendor/github.com/bluele/gcache/singleflight.go create mode 100644 vendor/github.com/bluele/gcache/stats.go create mode 100644 vendor/github.com/bluele/gcache/utils.go diff --git a/README.md b/README.md index ff36c26c..425258f0 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,11 @@ Runs a DNS-over-HTTPS proxy on `127.0.0.1:443`. ./dnsproxy -l 127.0.0.1 --https-port=443 --tls-crt=example.crt --tls-key=example.key -u 8.8.8.8:53 -p 0 ``` +Runs a DNS-over-HTTPS proxy on `127.0.0.1:443` with HTTP/3 support. +```shell +./dnsproxy -l 127.0.0.1 --https-port=443 --http3 --tls-crt=example.crt --tls-key=example.key -u 8.8.8.8:53 -p 0 +``` + Runs a DNS-over-QUIC proxy on `127.0.0.1:853`. ```shell ./dnsproxy -l 127.0.0.1 --quic-port=853 --tls-crt=example.crt --tls-key=example.key -u 8.8.8.8:53 -p 0 diff --git a/fastip/fastest.go b/fastip/fastest.go index 685cbd32..c30d4333 100644 --- a/fastip/fastest.go +++ b/fastip/fastest.go @@ -6,11 +6,10 @@ import ( "sync" "time" - "github.com/AdguardTeam/golibs/log" - "github.com/AdguardTeam/dnsproxy/proxyutil" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/cache" + "github.com/AdguardTeam/golibs/log" "github.com/miekg/dns" ) diff --git a/fastip/ping_test.go b/fastip/ping_test.go index 23fc851e..be1cf260 100644 --- a/fastip/ping_test.go +++ b/fastip/ping_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/AdguardTeam/golibs/netutil" + "github.com/AdguardTeam/golibs/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -99,7 +100,7 @@ func TestFastestAddr_PingAll_cache(t *testing.T) { t.Run("not_cached", func(t *testing.T) { listener, err := net.Listen("tcp", ":0") require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, listener.Close()) }) + testutil.CleanupAndRequireSuccess(t, listener.Close) ip := net.IP{127, 0, 0, 1} f := NewFastestAddr() @@ -138,8 +139,7 @@ func listen(t *testing.T, ip net.IP) (port uint) { l, err := net.Listen("tcp", netutil.IPPort{IP: ip, Port: 0}.String()) require.NoError(t, err) - - t.Cleanup(func() { require.NoError(t, l.Close()) }) + testutil.CleanupAndRequireSuccess(t, l.Close) return uint(l.Addr().(*net.TCPAddr).Port) } diff --git a/go.mod b/go.mod index 52232f05..6485dd61 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/ameshkov/dnscrypt/v2 v2.2.5 github.com/ameshkov/dnsstamps v1.0.3 github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 + github.com/bluele/gcache v0.0.2 github.com/jessevdk/go-flags v1.5.0 github.com/lucas-clemente/quic-go v0.29.0 github.com/miekg/dns v1.1.50 diff --git a/go.sum b/go.sum index 484a8304..1bff13de 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= +github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= +github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/main.go b/main.go index df92091f..6474e2c9 100644 --- a/main.go +++ b/main.go @@ -81,8 +81,7 @@ type Options struct { DNSCryptConfigPath string `yaml:"dnscrypt-config" short:"g" long:"dnscrypt-config" description:"Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt"` // HTTP3 controls whether HTTP/3 is enabled for this instance of dnsproxy. - // At this point it only enables it for upstreams, but in the future it will - // also enable it for the server. + // It enables HTTP/3 support for both the DoH upstreams and the DoH server. HTTP3 bool `yaml:"http3" long:"http3" description:"Enable HTTP/3 support" optional:"yes" optional-value:"false"` // Upstream DNS servers settings @@ -274,6 +273,7 @@ func createProxyConfig(options *Options) proxy.Config { CacheMaxTTL: options.CacheMaxTTL, CacheOptimistic: options.CacheOptimistic, RefuseAny: options.RefuseAny, + HTTP3: options.HTTP3, // TODO(e.burkov): The following CIDRs are aimed to match any // address. This is not quite proper approach to be used by // default so think about configuring it. diff --git a/proxy/config.go b/proxy/config.go index 0e10bdd9..f9e1bfe4 100644 --- a/proxy/config.go +++ b/proxy/config.go @@ -54,6 +54,7 @@ type Config struct { // -- TLSConfig *tls.Config // necessary for TLS, HTTPS, QUIC + HTTP3 bool // if true, HTTPS server will also support HTTP/3 DNSCryptProviderName string // DNSCrypt provider name DNSCryptResolverCert *dnscrypt.Cert // DNSCrypt resolver certificate diff --git a/proxy/proxy.go b/proxy/proxy.go index 816a26a8..8528ab9d 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -4,6 +4,7 @@ package proxy import ( "fmt" + "io" "net" "net/http" "sync" @@ -18,6 +19,7 @@ import ( "github.com/AdguardTeam/golibs/netutil" "github.com/ameshkov/dnscrypt/v2" "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" "github.com/miekg/dns" gocache "github.com/patrickmn/go-cache" ) @@ -65,15 +67,17 @@ type Proxy struct { // Listeners // -- - udpListen []*net.UDPConn // UDP listen connections - tcpListen []net.Listener // TCP listeners - tlsListen []net.Listener // TLS listeners - quicListen []quic.Listener // QUIC listeners - httpsListen []net.Listener // HTTPS listeners - httpsServer []*http.Server // HTTPS server instance - dnsCryptUDPListen []*net.UDPConn // UDP listen connections for DNSCrypt - dnsCryptTCPListen []net.Listener // TCP listeners for DNSCrypt - dnsCryptServer *dnscrypt.Server // DNSCrypt server instance + udpListen []*net.UDPConn // UDP listen connections + tcpListen []net.Listener // TCP listeners + tlsListen []net.Listener // TLS listeners + quicListen []quic.EarlyListener // QUIC listeners + httpsListen []net.Listener // HTTPS listeners + httpsServer *http.Server // HTTPS server instance + h3Listen []quic.EarlyListener // HTTP/3 listeners + h3Server *http3.Server // HTTP/3 server instance + dnsCryptUDPListen []*net.UDPConn // UDP listen connections for DNSCrypt + dnsCryptTCPListen []net.Listener // TCP listeners for DNSCrypt + dnsCryptServer *dnscrypt.Server // DNSCrypt server instance // Upstream // -- @@ -145,19 +149,6 @@ func (p *Proxy) Init() (err error) { p.requestGoroutinesSema = newNoopSemaphore() } - if p.DNSCryptResolverCert != nil && p.DNSCryptProviderName != "" { - log.Info("Initializing DNSCrypt: %s", p.DNSCryptProviderName) - p.dnsCryptServer = &dnscrypt.Server{ - ProviderName: p.DNSCryptProviderName, - ResolverCert: p.DNSCryptResolverCert, - Handler: &dnsCryptHandler{ - proxy: p, - - requestGoroutinesSema: p.requestGoroutinesSema, - }, - } - } - p.udpOOBSize = proxyutil.UDPGetOOBSize() p.bytesPool = &sync.Pool{ New: func() interface{} { @@ -212,6 +203,17 @@ func (p *Proxy) Start() (err error) { return nil } +// closeAll closes all elements in the toClose slice and if there's any error +// appends it to the errs slice. +func closeAll[T io.Closer](toClose []T, errs *[]error) { + for _, c := range toClose { + err := c.Close() + if err != nil { + *errs = append(*errs, err) + } + } +} + // Stop stops the proxy server including all its listeners func (p *Proxy) Stop() error { log.Info("Stopping the DNS proxy server") @@ -225,61 +227,38 @@ func (p *Proxy) Stop() error { errs := []error{} - for _, l := range p.tcpListen { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("closing tcp listening socket: %w", err)) - } - } + closeAll(p.tcpListen, &errs) p.tcpListen = nil - for _, l := range p.udpListen { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("closing udp listening socket: %w", err)) - } - } + closeAll(p.udpListen, &errs) p.udpListen = nil - for _, l := range p.tlsListen { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("closing tls listening socket: %w", err)) - } - } + closeAll(p.tlsListen, &errs) p.tlsListen = nil - for _, srv := range p.httpsServer { - err := srv.Close() - if err != nil { - errs = append(errs, fmt.Errorf("closing https server: %w", err)) - } + if p.httpsServer != nil { + closeAll([]io.Closer{p.httpsServer}, &errs) + p.httpsServer = nil + + // No need to close these since they're closed by httpsServer.Close(). + p.httpsListen = nil } - p.httpsListen = nil - p.httpsServer = nil - for _, l := range p.quicListen { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("closing quic listener: %w", err)) - } + if p.h3Server != nil { + closeAll([]io.Closer{p.h3Server}, &errs) + p.h3Server = nil } + + closeAll(p.h3Listen, &errs) + p.h3Listen = nil + + closeAll(p.quicListen, &errs) p.quicListen = nil - for _, l := range p.dnsCryptUDPListen { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("closing dnscrypt udp listening socket: %w", err)) - } - } + closeAll(p.dnsCryptUDPListen, &errs) p.dnsCryptUDPListen = nil - for _, l := range p.dnsCryptTCPListen { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("closing dnscrypt tcp listening socket: %w", err)) - } - } + closeAll(p.dnsCryptTCPListen, &errs) p.dnsCryptTCPListen = nil p.started = false diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index e68cd972..6596d131 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -736,9 +736,7 @@ func TestResponseInRequest(t *testing.T) { func TestNoQuestion(t *testing.T) { dnsProxy := createTestProxy(t, nil) require.NoError(t, dnsProxy.Start()) - t.Cleanup(func() { - require.NoError(t, dnsProxy.Stop()) - }) + testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop) addr := dnsProxy.Addr(ProtoUDP) client := &dns.Client{Net: "udp", Timeout: 500 * time.Millisecond} @@ -780,9 +778,7 @@ func (wu *funcUpstream) Address() string { func TestProxy_ReplyFromUpstream_badResponse(t *testing.T) { dnsProxy := createTestProxy(t, nil) require.NoError(t, dnsProxy.Start()) - t.Cleanup(func() { - require.NoError(t, dnsProxy.Stop()) - }) + testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop) exchangeFunc := func(m *dns.Msg) (resp *dns.Msg, err error) { resp = &dns.Msg{} diff --git a/proxy/server.go b/proxy/server.go index e9fc66c7..a406b4ae 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -6,6 +6,7 @@ import ( "time" "github.com/AdguardTeam/golibs/log" + "github.com/lucas-clemente/quic-go" "github.com/miekg/dns" ) @@ -53,8 +54,12 @@ func (p *Proxy) startListeners() error { go p.tcpPacketLoop(l, ProtoTLS, p.requestGoroutinesSema) } - for i := range p.httpsServer { - go p.listenHTTPS(p.httpsServer[i], p.httpsListen[i]) + for _, l := range p.httpsListen { + go func(l net.Listener) { _ = p.httpsServer.Serve(l) }(l) + } + + for _, l := range p.h3Listen { + go func(l quic.EarlyListener) { _ = p.h3Server.ServeListener(l) }(l) } for _, l := range p.quicListen { diff --git a/proxy/server_dnscrypt.go b/proxy/server_dnscrypt.go index 308d068a..afc7b68f 100644 --- a/proxy/server_dnscrypt.go +++ b/proxy/server_dnscrypt.go @@ -4,12 +4,33 @@ import ( "fmt" "net" + "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/ameshkov/dnscrypt/v2" "github.com/miekg/dns" ) -func (p *Proxy) createDNSCryptListeners() error { +func (p *Proxy) createDNSCryptListeners() (err error) { + if len(p.DNSCryptUDPListenAddr) == 0 && len(p.DNSCryptTCPListenAddr) == 0 { + // Do nothing if DNSCrypt listen addresses are not specified. + return nil + } + + if p.DNSCryptResolverCert == nil || p.DNSCryptProviderName == "" { + return errors.Error("invalid DNSCrypt configuration: no certificate or provider name") + } + + log.Info("Initializing DNSCrypt: %s", p.DNSCryptProviderName) + p.dnsCryptServer = &dnscrypt.Server{ + ProviderName: p.DNSCryptProviderName, + ResolverCert: p.DNSCryptResolverCert, + Handler: &dnsCryptHandler{ + proxy: p, + + requestGoroutinesSema: p.requestGoroutinesSema, + }, + } + for _, a := range p.DNSCryptUDPListenAddr { log.Info("Creating a DNSCrypt UDP listener") udpListen, err := net.ListenUDP("udp", a) diff --git a/proxy/server_https.go b/proxy/server_https.go index 7324fc14..0acad3e2 100644 --- a/proxy/server_https.go +++ b/proxy/server_https.go @@ -1,6 +1,7 @@ package proxy import ( + "crypto/tls" "encoding/base64" "fmt" "io" @@ -11,56 +12,107 @@ import ( "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" "github.com/miekg/dns" "golang.org/x/net/http2" ) -func (p *Proxy) createHTTPSListeners() error { - for _, a := range p.HTTPSListenAddr { - log.Info("Creating an HTTPS server") - tcpListen, err := net.ListenTCP("tcp", a) - if err != nil { - return fmt.Errorf("starting https listener: %w", err) - } - p.httpsListen = append(p.httpsListen, tcpListen) - log.Info("Listening to https://%s", tcpListen.Addr()) +// listenHTTP creates instances of TLS listeners that will be used to run an +// H1/H2 server. Returns the address the listener actually listens to (useful +// in the case if port 0 is specified). +func (p *Proxy) listenHTTP(addr *net.TCPAddr) (laddr *net.TCPAddr, err error) { + tcpListen, err := net.ListenTCP("tcp", addr) + if err != nil { + return nil, fmt.Errorf("tcp listener: %w", err) + } + log.Info("Listening to https://%s", tcpListen.Addr()) - tlsConfig := p.TLSConfig.Clone() - tlsConfig.NextProtos = []string{http2.NextProtoTLS, "http/1.1"} + tlsConfig := p.TLSConfig.Clone() + tlsConfig.NextProtos = []string{http2.NextProtoTLS, "http/1.1"} - srv := &http.Server{ - TLSConfig: tlsConfig, - Handler: p, - ReadHeaderTimeout: defaultTimeout, - WriteTimeout: defaultTimeout, - } + tlsListen := tls.NewListener(tcpListen, tlsConfig) + p.httpsListen = append(p.httpsListen, tlsListen) - p.httpsServer = append(p.httpsServer, srv) + return tcpListen.Addr().(*net.TCPAddr), nil +} + +// listenH3 creates instances of QUIC listeners that will be used for running +// an HTTP/3 server. +func (p *Proxy) listenH3(addr *net.UDPAddr) (err error) { + tlsConfig := p.TLSConfig.Clone() + tlsConfig.NextProtos = []string{"h3"} + quicListen, err := quic.ListenAddrEarly(addr.String(), tlsConfig, newServerQUICConfig()) + if err != nil { + return fmt.Errorf("quic listener: %w", err) } + log.Info("Listening to h3://%s", quicListen.Addr()) + + p.h3Listen = append(p.h3Listen, quicListen) return nil } -// serveHttps starts the HTTPS server -func (p *Proxy) listenHTTPS(srv *http.Server, l net.Listener) { - log.Info("Listening to DNS-over-HTTPS on %s", l.Addr()) - err := srv.ServeTLS(l, "", "") +// createHTTPSListeners creates TCP/UDP listeners and HTTP/H3 servers. +func (p *Proxy) createHTTPSListeners() (err error) { + p.httpsServer = &http.Server{ + Handler: &proxyHTTPHandler{ + proxy: p, + h3: false, + }, + ReadHeaderTimeout: defaultTimeout, + WriteTimeout: defaultTimeout, + } + + if p.HTTP3 { + p.h3Server = &http3.Server{ + Handler: &proxyHTTPHandler{ + proxy: p, + h3: true, + }, + } + } + + for _, addr := range p.HTTPSListenAddr { + log.Info("Creating an HTTPS server") - if err != http.ErrServerClosed { - log.Info("HTTPS server was closed unexpectedly: %s", err) - } else { - log.Info("HTTPS server was closed") + tcpAddr, err := p.listenHTTP(addr) + if err != nil { + return fmt.Errorf("failed to start HTTPS server on %s: %w", addr, err) + } + + if p.HTTP3 { + // HTTP/3 server listens to the same pair IP:port as the one HTTP/2 + // server listens to. + udpAddr := &net.UDPAddr{IP: tcpAddr.IP, Port: tcpAddr.Port} + err = p.listenH3(udpAddr) + if err != nil { + return fmt.Errorf("failed to start HTTP/3 server on %s: %w", udpAddr, err) + } + } } + + return nil +} + +// proxyHTTPHandler implements http.Handler and processes DoH queries. +type proxyHTTPHandler struct { + // h3 is true if this is an HTTP/3 requests handler. + h3 bool + proxy *Proxy } -// ServeHTTP is the http.RequestHandler implementation that handles DoH queries +// type check +var _ http.Handler = &proxyHTTPHandler{} + +// ServeHTTP is the http.Handler implementation that handles DoH queries. // Here is what it returns: // // - http.StatusBadRequest if there is no DNS request data; // - http.StatusUnsupportedMediaType if request content type is not // "application/dns-message"; // - http.StatusMethodNotAllowed if request method is not GET or POST. -func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *proxyHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Tracef("Incoming HTTPS request on %s", r.URL) var buf []byte @@ -103,12 +155,12 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - addr, prx, err := remoteAddr(r) + addr, prx, err := remoteAddr(r, h.h3) if err != nil { log.Debug("warning: getting real ip: %s", err) } - d := p.newDNSContext(ProtoHTTPS, req) + d := h.proxy.newDNSContext(ProtoHTTPS, req) d.Addr = addr d.HTTPRequest = r d.HTTPResponseWriter = w @@ -116,13 +168,13 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if prx != nil { ip, _ := netutil.IPAndPortFromAddr(prx) log.Debug("request came from proxy server %s", prx) - if !p.proxyVerifier.Contains(ip) { + if !h.proxy.proxyVerifier.Contains(ip) { log.Debug("proxy %s is not trusted, using original remote addr", ip) d.Addr = prx } } - err = p.handleDNSRequest(d) + err = h.proxy.handleDNSRequest(d) if err != nil { log.Tracef("error handling DNS (%s) request: %s", d.Proto, err) } @@ -187,7 +239,7 @@ func realIPFromHdrs(r *http.Request) (realIP net.IP) { // remoteAddr returns the real client's address and the IP address of the latest // proxy server if any. -func remoteAddr(r *http.Request) (addr, prx net.Addr, err error) { +func remoteAddr(r *http.Request, h3 bool) (addr, prx net.Addr, err error) { var hostStr, portStr string if hostStr, portStr, err = net.SplitHostPort(r.RemoteAddr); err != nil { return nil, nil, err @@ -206,13 +258,15 @@ func remoteAddr(r *http.Request) (addr, prx net.Addr, err error) { if realIP := realIPFromHdrs(r); realIP != nil { log.Tracef("Using IP address from HTTP request: %s", realIP) - // TODO(a.garipov): Use net.UDPAddr here and below when - // necessary when we start supporting HTTP/3. - // // TODO(a.garipov): Add port if we can get it from headers like // X-Real-Port, X-Forwarded-Port, etc. - addr = &net.TCPAddr{IP: realIP, Port: 0} - prx = &net.TCPAddr{IP: host, Port: port} + if h3 { + addr = &net.UDPAddr{IP: realIP, Port: 0} + prx = &net.UDPAddr{IP: host, Port: port} + } else { + addr = &net.TCPAddr{IP: realIP, Port: 0} + prx = &net.TCPAddr{IP: host, Port: port} + } return addr, prx, nil } diff --git a/proxy/server_https_test.go b/proxy/server_https_test.go index 5685f188..68d14289 100644 --- a/proxy/server_https_test.go +++ b/proxy/server_https_test.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "fmt" "io" "net" "net/http" @@ -12,12 +13,53 @@ import ( "testing" "github.com/AdguardTeam/golibs/netutil" + "github.com/AdguardTeam/golibs/testutil" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" "github.com/miekg/dns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHttpsProxy(t *testing.T) { + testCases := []struct { + name string + http3 bool + }{{ + name: "https_proxy", + http3: false, + }, { + name: "h3_proxy", + http3: true, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Prepare dnsProxy with its configuration. + tlsConf, caPem := createServerTLSConfig(t) + dnsProxy := createTestProxy(t, tlsConf) + dnsProxy.HTTP3 = tc.http3 + + // Run the proxy. + err := dnsProxy.Start() + require.NoError(t, err) + testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop) + + // Create the HTTP client that we'll be using for this test. + client := createTestHTTPClient(dnsProxy, caPem, tc.http3) + + // Prepare a test message to be sent to the server. + msg := createTestMessage() + + // Send the test message and check if the response is what we + // expected. + resp := sendTestDoHMessage(t, client, msg, nil) + requireResponse(t, msg, resp) + }) + } +} + +func TestHttpsProxyTrustedProxies(t *testing.T) { // Prepare the proxy server. tlsConf, caPem := createServerTLSConfig(t) dnsProxy := createTestProxy(t, tlsConf) @@ -29,30 +71,7 @@ func TestHttpsProxy(t *testing.T) { return dnsProxy.Resolve(d) } - roots := x509.NewCertPool() - ok := roots.AppendCertsFromPEM(caPem) - require.True(t, ok) - - dialer := &net.Dialer{ - Timeout: defaultTimeout, - } - dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { - // Route request to the DNS-over-HTTPS server address. - return dialer.DialContext(ctx, network, dnsProxy.Addr(ProtoHTTPS).String()) - } - - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - ServerName: tlsServerName, - RootCAs: roots, - }, - DisableCompression: true, - DialContext: dialContext, - } - client := http.Client{ - Transport: transport, - Timeout: defaultTimeout, - } + client := createTestHTTPClient(dnsProxy, caPem, false) clientIP, proxyIP := net.IP{1, 2, 3, 4}, net.IP{127, 0, 0, 1} msg := createTestMessage() @@ -63,42 +82,14 @@ func TestHttpsProxy(t *testing.T) { // Start listening. serr := dnsProxy.Start() require.NoError(t, serr) - t.Cleanup(func() { - derr := dnsProxy.Stop() - require.NoError(t, derr) - }) + testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop) - packed, err := msg.Pack() - require.NoError(t, err) - - b := bytes.NewBuffer(packed) - req, err := http.NewRequest("POST", "https://test.com", b) - require.NoError(t, err) - - req.Header.Set("Content-Type", "application/dns-message") - req.Header.Set("Accept", "application/dns-message") - // IP "1.2.3.4" will be used as a client address in DNSContext. - req.Header.Set("X-Forwarded-For", strings.Join( - []string{clientIP.String(), proxyIP.String()}, - ",", - )) - - resp, err := client.Do(req) - require.NoError(t, err) - - if resp != nil && resp.Body != nil { - t.Cleanup(func() { - resp.Body.Close() - }) + hdrs := map[string]string{ + "X-Forwarded-For": strings.Join([]string{clientIP.String(), proxyIP.String()}, ","), } - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - reply := &dns.Msg{} - err = reply.Unpack(body) - require.NoError(t, err) - requireResponse(t, msg, reply) + resp := sendTestDoHMessage(t, client, msg, hdrs) + requireResponse(t, msg, resp) } t.Run("success", func(t *testing.T) { @@ -300,7 +291,7 @@ func TestRemoteAddr(t *testing.T) { } t.Run(tc.name, func(t *testing.T) { - addr, prx, err := remoteAddr(r) + addr, prx, err := remoteAddr(r, false) if tc.wantErr != "" { assert.Equal(t, tc.wantErr, err.Error()) @@ -317,3 +308,90 @@ func TestRemoteAddr(t *testing.T) { }) } } + +// sendTestDoHMessage sends the specified DNS message using client and returns +// the DNS response. +func sendTestDoHMessage( + t *testing.T, + client *http.Client, + m *dns.Msg, + hdrs map[string]string, +) (resp *dns.Msg) { + packed, err := m.Pack() + require.NoError(t, err) + + b := bytes.NewBuffer(packed) + u := fmt.Sprintf("https://%s/dns-query", tlsServerName) + req, err := http.NewRequest(http.MethodPost, u, b) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/dns-message") + req.Header.Set("Accept", "application/dns-message") + + for k, v := range hdrs { + req.Header.Set(k, v) + } + + httpResp, err := client.Do(req) + require.NoError(t, err) + testutil.CleanupAndRequireSuccess(t, httpResp.Body.Close) + + body, err := io.ReadAll(httpResp.Body) + require.NoError(t, err) + + resp = &dns.Msg{} + err = resp.Unpack(body) + require.NoError(t, err) + + return resp +} + +// createTestHTTPClient creates an *http.Client that will be used to send +// requests to the specified dnsProxy. +func createTestHTTPClient(dnsProxy *Proxy, caPem []byte, http3Enabled bool) (client *http.Client) { + // prepare roots list so that the server cert was successfully validated. + roots := x509.NewCertPool() + roots.AppendCertsFromPEM(caPem) + tlsClientConfig := &tls.Config{ + ServerName: tlsServerName, + RootCAs: roots, + } + + var transport http.RoundTripper + + if http3Enabled { + transport = &http3.RoundTripper{ + Dial: func( + ctx context.Context, + _ string, + tlsCfg *tls.Config, + cfg *quic.Config, + ) (quic.EarlyConnection, error) { + addr := dnsProxy.Addr(ProtoHTTPS).String() + return quic.DialAddrEarlyContext(ctx, addr, tlsCfg, cfg) + }, + TLSClientConfig: tlsClientConfig, + QuicConfig: &quic.Config{}, + DisableCompression: true, + } + } else { + dialer := &net.Dialer{ + Timeout: defaultTimeout, + } + dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { + // Route request to the DNS-over-HTTPS server address. + return dialer.DialContext(ctx, network, dnsProxy.Addr(ProtoHTTPS).String()) + } + + transport = &http.Transport{ + TLSClientConfig: tlsClientConfig, + DisableCompression: true, + DialContext: dialContext, + } + } + + return &http.Client{ + Transport: transport, + Timeout: defaultTimeout, + } +} diff --git a/proxy/server_quic.go b/proxy/server_quic.go index 59958f3e..ce8adc23 100644 --- a/proxy/server_quic.go +++ b/proxy/server_quic.go @@ -5,12 +5,14 @@ import ( "encoding/binary" "errors" "fmt" - "strings" + "io" + "math" + "net" "time" "github.com/AdguardTeam/dnsproxy/proxyutil" "github.com/AdguardTeam/golibs/log" - "github.com/AdguardTeam/golibs/stringutil" + "github.com/bluele/gcache" "github.com/lucas-clemente/quic-go" "github.com/miekg/dns" ) @@ -31,6 +33,18 @@ var compatProtoDQ = []string{NextProtoDQ, "doq-i02", "doq-i00", "dq"} // better for clients written with ngtcp2. const maxQUICIdleTimeout = 5 * time.Minute +// quicAddrValidatorCacheSize is the size of the cache that we use in the QUIC +// address validator. The value is chosen arbitrarily and we should consider +// making it configurable. +// TODO(ameshkov): make it configurable. +const quicAddrValidatorCacheSize = 1000 + +// quicAddrValidatorCacheTTL is time-to-live for cache items in the QUIC address +// validator. The value is chosen arbitrarily and we should consider making it +// configurable. +// TODO(ameshkov): make it configurable. +const quicAddrValidatorCacheTTL = 30 * time.Minute + const ( // DoQCodeNoError is used when the connection or stream needs to be closed, // but there is no error to signal. @@ -44,14 +58,19 @@ const ( DoQCodeProtocolError quic.ApplicationErrorCode = 2 ) +// createQUICListeners creates QUIC listeners for the DoQ server. func (p *Proxy) createQUICListeners() error { for _, a := range p.QUICListenAddr { log.Info("Creating a QUIC listener") tlsConfig := p.TLSConfig.Clone() tlsConfig.NextProtos = compatProtoDQ - quicListen, err := quic.ListenAddr(a.String(), tlsConfig, &quic.Config{MaxIdleTimeout: maxQUICIdleTimeout}) + quicListen, err := quic.ListenAddrEarly( + a.String(), + tlsConfig, + newServerQUICConfig(), + ) if err != nil { - return fmt.Errorf("starting quic listener: %w", err) + return fmt.Errorf("quic listener: %w", err) } p.quicListen = append(p.quicListen, quicListen) @@ -63,13 +82,14 @@ func (p *Proxy) createQUICListeners() error { // quicPacketLoop listens for incoming QUIC packets. // // See also the comment on Proxy.requestGoroutinesSema. -func (p *Proxy) quicPacketLoop(l quic.Listener, requestGoroutinesSema semaphore) { +func (p *Proxy) quicPacketLoop(l quic.EarlyListener, requestGoroutinesSema semaphore) { log.Info("Entering the DNS-over-QUIC listener loop on %s", l.Addr()) for { conn, err := l.Accept(context.Background()) + if err != nil { if isQUICNonCrit(err) { - log.Tracef("quic connection closed or timeout: %s", err) + log.Tracef("quic connection closed or timed out: %s", err) } else { log.Error("reading from quic listen: %s", err) } @@ -140,7 +160,10 @@ func (p *Proxy) handleQUICStream(stream quic.Stream, conn quic.Connection) { buf := *bufPtr n, err := stream.Read(buf) - if n < minDNSPacketSize { + // Note that io.EOF does not really mean that there's any error, this is + // just a signal that there will be no data to read anymore from this + // stream. + if (err != nil && err != io.EOF) || n < minDNSPacketSize { logShortQUICRead(err) return @@ -295,20 +318,42 @@ func logShortQUICRead(err error) { } // isQUICNonCrit returns true if err is a non-critical error, most probably -// a timeout or a closed connection. -// -// TODO(a.garipov): Inspect and rewrite with modern error handling. +// related to the current QUIC implementation. +// TODO(ameshkov): re-test when updating quic-go. func isQUICNonCrit(err error) (ok bool) { if err == nil { return false } - errStr := err.Error() + if errors.Is(err, quic.ErrServerClosed) { + // This error is returned when the QUIC listener was closed by us. This + // is an expected error, we don't need the detailed logs here. + return true + } + + var qAppErr *quic.ApplicationError + if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { + // This error is returned when a QUIC connection was gracefully closed. + // No need to have detailed logs for it either. + return true + } - return strings.Contains(errStr, "server closed") || - stringutil.ContainsFold(errStr, "no recent network activity") || - strings.HasSuffix(errStr, "Application error 0x0") || - errStr == "EOF" + if errors.Is(err, quic.Err0RTTRejected) { + // This error is returned on AcceptStream calls when the server rejects + // 0-RTT for some reason. This is a common scenario, no need for extra + // logs. + return true + } + + var qIdleErr *quic.IdleTimeoutError + if errors.As(err, &qIdleErr) { + // This error is returned when we're trying to accept a new stream from + // a connection that had no activity for over than the keep-alive + // timeout. This is a common scenario, no need for extra logs. + return true + } + + return false } // closeQUICConn quietly closes the QUIC connection. @@ -318,3 +363,51 @@ func closeQUICConn(conn quic.Connection, code quic.ApplicationErrorCode) { log.Debug("failed to close QUIC connection: %v", err) } } + +// newServerQUICConfig creates *quic.Config populated with the default settings. +// This function is supposed to be used for both DoQ and DoH3 server. +func newServerQUICConfig() (conf *quic.Config) { + v := newQUICAddrValidator(quicAddrValidatorCacheSize, quicAddrValidatorCacheTTL) + + return &quic.Config{ + MaxIdleTimeout: maxQUICIdleTimeout, + RequireAddressValidation: v.requiresValidation, + MaxIncomingStreams: math.MaxUint16, + MaxIncomingUniStreams: math.MaxUint16, + } +} + +// quicAddrValidator is a helper struct that holds a small LRU cache of +// addresses for which we do not require address validation. +type quicAddrValidator struct { + cache gcache.Cache + ttl time.Duration +} + +// newQUICAddrValidator initializes a new instance of *quicAddrValidator. +func newQUICAddrValidator(cacheSize int, ttl time.Duration) (v *quicAddrValidator) { + return &quicAddrValidator{ + cache: gcache.New(cacheSize).LRU().Build(), + ttl: ttl, + } +} + +// requiresValidation determines if a QUIC Retry packet should be sent by the +// client. This allows the server to verify the client's address but increases +// the latency. +func (v *quicAddrValidator) requiresValidation(addr net.Addr) (ok bool) { + key := addr.String() + if v.cache.Has(key) { + return false + } + + err := v.cache.SetWithExpire(key, true, v.ttl) + if err != nil { + // Shouldn't happen, since we don't set a serialization function. + panic(fmt.Errorf("quic validator: setting cache item: %w", err)) + } + + // Address not found in the cache so return true to make sure the server + // will require address validation. + return true +} diff --git a/proxy/server_quic_test.go b/proxy/server_quic_test.go index 3896dad0..5d93f84e 100644 --- a/proxy/server_quic_test.go +++ b/proxy/server_quic_test.go @@ -34,7 +34,7 @@ func TestQuicProxy(t *testing.T) { addr := dnsProxy.Addr(ProtoQUIC) // Open QUIC connection. - conn, err := quic.DialAddr(addr.String(), tlsConfig, nil) + conn, err := quic.DialAddrEarly(addr.String(), tlsConfig, nil) require.NoError(t, err) defer conn.CloseWithError(DoQCodeNoError, "") diff --git a/upstream/bootstrap.go b/upstream/bootstrap.go index a5e70edb..4568732d 100755 --- a/upstream/bootstrap.go +++ b/upstream/bootstrap.go @@ -306,3 +306,16 @@ func (n *bootstrapper) createDialContext(addresses []string) (dialContext dialHa return nil, errors.List("all dialers failed", errs...) } } + +// newContext creates a new context with deadline if needed. If no timeout is +// set cancel would be a simple noop. +func (n *bootstrapper) newContext() (ctx context.Context, cancel context.CancelFunc) { + ctx = context.Background() + cancel = func() {} + + if n.options.Timeout > 0 { + ctx, cancel = context.WithDeadline(ctx, time.Now().Add(n.options.Timeout)) + } + + return ctx, cancel +} diff --git a/upstream/upstream_doh.go b/upstream/upstream_doh.go index 78a93afa..b8868aa6 100644 --- a/upstream/upstream_doh.go +++ b/upstream/upstream_doh.go @@ -9,7 +9,6 @@ import ( "net" "net/http" "net/url" - "os" "sync" "time" @@ -74,12 +73,7 @@ func newDoH(uu *url.URL, opts *Options) (u Upstream, err error) { quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, - // You can read more on address validation here: - // https://datatracker.ietf.org/doc/html/rfc9000#section-8.1 - // Setting maxOrigins to 1 and tokensPerOrigin to 10 assuming that - // this is more than enough for the way we use it (one connection - // per upstream). - TokenStore: quic.NewLRUTokenStore(1, 10), + TokenStore: newQUICTokenStore(), }, }, nil } @@ -88,17 +82,69 @@ func newDoH(uu *url.URL, opts *Options) (u Upstream, err error) { func (p *dnsOverHTTPS) Address() string { return p.boot.URL.String() } // Exchange implements the Upstream interface for *dnsOverHTTPS. -func (p *dnsOverHTTPS) Exchange(m *dns.Msg) (*dns.Msg, error) { +func (p *dnsOverHTTPS) Exchange(m *dns.Msg) (resp *dns.Msg, err error) { + // Quote from https://www.rfc-editor.org/rfc/rfc8484.html: + // In order to maximize HTTP cache friendliness, DoH clients using media + // formats that include the ID field from the DNS message header, such + // as "application/dns-message", SHOULD use a DNS ID of 0 in every DNS + // request. + id := m.Id + m.Id = 0 + defer func() { + // Restore the original ID to not break compatibility with proxies. + m.Id = id + if resp != nil { + resp.Id = id + } + }() + + // Check if there was already an active client before sending the request. + // We'll only attempt to re-connect if there was one. + hasClient := p.hasClient() + + // Make the first attempt to send the DNS query. + resp, err = p.exchangeHTTPS(m) + + // Make up to 2 attempts to re-create the HTTP client and send the request + // again. There are several cases (mostly, with QUIC) where this workaround + // is necessary to make HTTP client usable. We need to make 2 attempts in + // the case when the connection was closed (due to inactivity for example) + // AND the server refuses to open a 0-RTT connection. + for i := 0; hasClient && p.shouldRetry(err) && i < 2; i++ { + log.Debug("re-creating the HTTP client and retrying due to %v", err) + + p.clientGuard.Lock() + p.client = nil + // Re-create the token store to make sure we're not trying to use invalid + // tokens for 0-RTT. + p.quicConfig.TokenStore = newQUICTokenStore() + p.clientGuard.Unlock() + + resp, err = p.exchangeHTTPS(m) + } + + if err != nil { + // If the request failed anyway, make sure we don't use this client. + p.clientGuard.Lock() + p.client = nil + p.clientGuard.Unlock() + } + + return resp, err +} + +// exchangeHTTPS creates an HTTP client and sends the DNS query using it. +func (p *dnsOverHTTPS) exchangeHTTPS(m *dns.Msg) (resp *dns.Msg, err error) { client, err := p.getClient() if err != nil { return nil, fmt.Errorf("initializing http client: %w", err) } logBegin(p.Address(), m) - r, err := p.exchangeHTTPSClient(m, client) + resp, err = p.exchangeHTTPSClient(m, client) logFinish(p.Address(), err) - return r, err + return resp, err } // exchangeHTTPSClient sends the DNS query to a DoH resolver using the specified @@ -125,16 +171,6 @@ func (p *dnsOverHTTPS) exchangeHTTPSClient(m *dns.Msg, client *http.Client) (*dn defer resp.Body.Close() } if err != nil { - if errors.Is(err, os.ErrDeadlineExceeded) { - // If this is a timeout error, trying to forcibly re-create the HTTP - // client instance. - // - // See https://github.com/AdguardTeam/AdGuardHome/issues/3217. - p.clientGuard.Lock() - p.client = nil - p.clientGuard.Unlock() - } - return nil, fmt.Errorf("requesting %s: %w", p.boot.URL, err) } @@ -160,6 +196,38 @@ func (p *dnsOverHTTPS) exchangeHTTPSClient(m *dns.Msg, client *http.Client) (*dn return &response, err } +// hasClient returns true if this connection already has an active HTTP client. +func (p *dnsOverHTTPS) hasClient() (ok bool) { + p.clientGuard.Lock() + defer p.clientGuard.Unlock() + + return p.client != nil +} + +// shouldRetry checks what error we have received and returns true if we should +// re-create the HTTP client and retry the request. +func (p *dnsOverHTTPS) shouldRetry(err error) (ok bool) { + if err == nil { + return false + } + + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + // If this is a timeout error, trying to forcibly re-create the HTTP + // client instance. This is an attempt to fix an issue with DoH client + // stalling after a network change. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/3217. + return true + } + + if isQUICRetryError(err) { + return true + } + + return false +} + // getClient gets or lazily initializes an HTTP client (and transport) that will // be used for this DoH resolver. func (p *dnsOverHTTPS) getClient() (c *http.Client, err error) { @@ -266,7 +334,7 @@ func (p *dnsOverHTTPS) createTransport() (t http.RoundTripper, err error) { func (p *dnsOverHTTPS) createTransportH3( tlsConfig *tls.Config, dialContext dialHandler, -) (roundTripper *http3.RoundTripper, err error) { +) (roundTripper http.RoundTripper, err error) { if !p.supportsH3() { return nil, errors.Error("HTTP3 support is not enabled") } @@ -276,21 +344,25 @@ func (p *dnsOverHTTPS) createTransportH3( return nil, err } - return &http3.RoundTripper{ + rt := &http3.RoundTripper{ Dial: func( ctx context.Context, + // Ignore the address and always connect to the one that we got // from the bootstrapper. _ string, tlsCfg *tls.Config, cfg *quic.Config, ) (c quic.EarlyConnection, err error) { - return quic.DialAddrEarlyContext(ctx, addr, tlsCfg, cfg) + c, err = quic.DialAddrEarlyContext(ctx, addr, tlsCfg, cfg) + return c, err }, DisableCompression: true, TLSClientConfig: tlsConfig, QuicConfig: p.quicConfig, - }, nil + } + + return rt, nil } // probeH3 runs a test to check whether QUIC is faster than TLS for this diff --git a/upstream/upstream_doh_test.go b/upstream/upstream_doh_test.go index 16a9a09f..2e0075e3 100644 --- a/upstream/upstream_doh_test.go +++ b/upstream/upstream_doh_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + "strconv" "testing" "time" @@ -105,15 +106,88 @@ func TestUpstreamDoH(t *testing.T) { } } +func TestUpstreamDoH_serverRestart(t *testing.T) { + testCases := []struct { + name string + httpVersions []HTTPVersion + }{ + { + name: "http2", + httpVersions: []HTTPVersion{HTTPVersion11, HTTPVersion2}, + }, + { + name: "http3", + httpVersions: []HTTPVersion{HTTPVersion3}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Run the first server instance. + srv := startDoHServer(t, testDoHServerOptions{ + http3Enabled: true, + }) + + // Create a DNS-over-HTTPS upstream. + address := fmt.Sprintf("https://%s/dns-query", srv.addr) + u, err := AddressToUpstream( + address, + &Options{ + InsecureSkipVerify: true, + HTTPVersions: tc.httpVersions, + Timeout: time.Second, + }, + ) + require.NoError(t, err) + + // Test that the upstream works properly. + checkUpstream(t, u, address) + + // Now let's restart the server on the same address. + _, portStr, err := net.SplitHostPort(srv.addr) + require.NoError(t, err) + port, err := strconv.Atoi(portStr) + + // Shutdown the first server. + srv.Shutdown() + + // Start the new one on the same port. + srv = startDoHServer(t, testDoHServerOptions{ + http3Enabled: true, + port: port, + }) + + // Check that everything works after restart. + checkUpstream(t, u, address) + + // Stop the server again. + srv.Shutdown() + + // Now try to send a message and make sure that it returns an error. + _, err = u.Exchange(createTestMessage()) + require.Error(t, err) + + // Start the server one more time. + srv = startDoHServer(t, testDoHServerOptions{ + http3Enabled: true, + port: port, + }) + + // Check that everything works after the second restart. + checkUpstream(t, u, address) + }) + } +} + // testDoHServerOptions allows customizing testDoHServer behavior. type testDoHServerOptions struct { http3Enabled bool delayHandshakeH2 time.Duration delayHandshakeH3 time.Duration + port int } -// testDoHServer is an instance of a test DNS-over-HTTPS server that we use -// for tests. +// testDoHServer is an instance of a test DNS-over-HTTPS server. type testDoHServer struct { // addr is the address that this server listens to. addr string @@ -126,9 +200,12 @@ type testDoHServer struct { // serverH3 is an HTTP/3 server. serverH3 *http3.Server + + // listenerH3 that's used to serve HTTP/3. + listenerH3 quic.EarlyListener } -// Shutdown stops the DOH server. +// Shutdown stops the DoH server. func (s *testDoHServer) Shutdown() { if s.server != nil { _ = s.server.Shutdown(context.Background()) @@ -136,6 +213,7 @@ func (s *testDoHServer) Shutdown() { if s.serverH3 != nil { _ = s.serverH3.Close() + _ = s.listenerH3.Close() } } @@ -156,7 +234,8 @@ func startDoHServer( } // Listen TCP first. - tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") + listenAddr := fmt.Sprintf("127.0.0.1:%d", opts.port) + tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr) require.NoError(t, err) tcpListen, err := net.ListenTCP("tcp", tcpAddr) @@ -179,6 +258,7 @@ func startDoHServer( tcpAddr = tcpListen.Addr().(*net.TCPAddr) var serverH3 *http3.Server + var listenerH3 quic.EarlyListener if opts.http3Enabled { tlsConfigH3 := tlsConfig.Clone() @@ -191,9 +271,7 @@ func startDoHServer( } serverH3 = &http3.Server{ - TLSConfig: tlsConfig.Clone(), - QuicConfig: &quic.Config{}, - Handler: handler, + Handler: handler, } // Listen UDP for the H3 server. Reuse the same port as was used for the @@ -201,17 +279,18 @@ func startDoHServer( udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("127.0.0.1:%d", tcpAddr.Port)) require.NoError(t, err) - udpListen, err := net.ListenUDP("udp", udpAddr) + listenerH3, err = quic.ListenAddrEarly(udpAddr.String(), tlsConfigH3, &quic.Config{}) require.NoError(t, err) // Run the H3 server. - go serverH3.Serve(udpListen) + go serverH3.ServeListener(listenerH3) } return &testDoHServer{ - tlsConfig: tlsConfig, - server: server, - serverH3: serverH3, + tlsConfig: tlsConfig, + server: server, + serverH3: serverH3, + listenerH3: listenerH3, // Save the address that the server listens to. addr: tcpAddr.String(), } diff --git a/upstream/upstream_quic.go b/upstream/upstream_quic.go index eb98fd17..f8942def 100644 --- a/upstream/upstream_quic.go +++ b/upstream/upstream_quic.go @@ -9,6 +9,7 @@ import ( "time" "github.com/AdguardTeam/dnsproxy/proxyutil" + "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/lucas-clemente/quic-go" "github.com/miekg/dns" @@ -22,9 +23,6 @@ const ( // an internal error and is incapable of pursuing the transaction or the // connection. QUICCodeInternalError = quic.ApplicationErrorCode(1) - // QUICCodeProtocolError signals that the DoQ implementation encountered - // a protocol error and is forcibly aborting the connection. - QUICCodeProtocolError = quic.ApplicationErrorCode(2) // QUICKeepAlivePeriod is the value that we pass to *quic.Config and that // controls the period with with keep-alive frames are being sent to the // connection. We set it to 20s as it would be in the quic-go@v0.27.1 with @@ -49,11 +47,12 @@ type dnsOverQUIC struct { // conn is the current active QUIC connection. It can be closed and // re-opened when needed. conn quic.Connection + // connGuard protects conn and quicConfig. + connGuard sync.RWMutex // bytesPool is a *sync.Pool we use to store byte buffers in. These byte // buffers are used to read responses from the upstream. - bytesPool *sync.Pool - // sync.RWMutex protects conn and bytesPool. - sync.RWMutex + bytesPool *sync.Pool + bytesPoolGuard sync.Mutex } // type check @@ -73,12 +72,7 @@ func newDoQ(uu *url.URL, opts *Options) (u Upstream, err error) { boot: b, quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, - // You can read more on address validation here: - // https://datatracker.ietf.org/doc/html/rfc9000#section-8.1 - // Setting maxOrigins to 1 and tokensPerOrigin to 10 assuming that - // this is more than enough for the way we use it (one connection - // per upstream). - TokenStore: quic.NewLRUTokenStore(1, 10), + TokenStore: newQUICTokenStore(), }, }, nil } @@ -87,25 +81,59 @@ func newDoQ(uu *url.URL, opts *Options) (u Upstream, err error) { func (p *dnsOverQUIC) Address() string { return p.boot.URL.String() } // Exchange implements the Upstream interface for *dnsOverQUIC. -func (p *dnsOverQUIC) Exchange(m *dns.Msg) (res *dns.Msg, err error) { - var conn quic.Connection - conn, err = p.getConnection(true) - if err != nil { - return nil, err - } - +func (p *dnsOverQUIC) Exchange(m *dns.Msg) (resp *dns.Msg, err error) { // When sending queries over a QUIC connection, the DNS Message ID MUST be // set to zero. id := m.Id m.Id = 0 defer func() { - // Restore the original ID to not break compatibility with proxies + // Restore the original ID to not break compatibility with proxies. m.Id = id - if res != nil { - res.Id = id + if resp != nil { + resp.Id = id } }() + // Check if there was already an active conn before sending the request. + // We'll only attempt to re-connect if there was one. + hasConnection := p.hasConnection() + + // Make the first attempt to send the DNS query. + resp, err = p.exchangeQUIC(m) + + // Make up to 2 attempts to re-open the QUIC connection and send the request + // again. There are several cases where this workaround is necessary to + // make DoQ usable. We need to make 2 attempts in the case when the + // connection was closed (due to inactivity for example) AND the server + // refuses to open a 0-RTT connection. + for i := 0; hasConnection && p.shouldRetry(err) && i < 2; i++ { + log.Debug("re-creating the QUIC connection and retrying due to %v", err) + + // Close the active connection to make sure we'll try to re-connect. + p.closeConnWithError(QUICCodeNoError) + + // Retry sending the request. + resp, err = p.exchangeQUIC(m) + } + + if err != nil { + // If we're unable to exchange messages, make sure the connection is + // closed and signal about an internal error. + p.closeConnWithError(QUICCodeInternalError) + } + + return resp, err +} + +// exchangeQUIC attempts to open a QUIC connection, send the DNS message +// through it and return the response it got from the server. +func (p *dnsOverQUIC) exchangeQUIC(m *dns.Msg) (resp *dns.Msg, err error) { + var conn quic.Connection + conn, err = p.getConnection(true) + if err != nil { + return nil, err + } + var buf []byte buf, err = m.Pack() if err != nil { @@ -115,36 +143,34 @@ func (p *dnsOverQUIC) Exchange(m *dns.Msg) (res *dns.Msg, err error) { var stream quic.Stream stream, err = p.openStream(conn) if err != nil { - p.closeConnWithError(QUICCodeInternalError) - return nil, fmt.Errorf("open new stream to %s: %w", p.Address(), err) + return nil, err } _, err = stream.Write(proxyutil.AddPrefix(buf)) if err != nil { - p.closeConnWithError(QUICCodeInternalError) return nil, fmt.Errorf("failed to write to a QUIC stream: %w", err) } // The client MUST send the DNS query over the selected stream, and MUST // indicate through the STREAM FIN mechanism that no further data will - // be sent on that stream. - // stream.Close() -- closes the write-direction of the stream. + // be sent on that stream. Note, that stream.Close() closes the + // write-direction of the stream, but does not prevent reading from it. _ = stream.Close() - res, err = p.readMsg(stream) - if err != nil { - // If a peer encounters such an error condition, it is considered a - // fatal error. It SHOULD forcibly abort the connection using QUIC's - // CONNECTION_CLOSE mechanism and SHOULD use the DoQ error code - // DOQ_PROTOCOL_ERROR. - p.closeConnWithError(QUICCodeProtocolError) - } - return res, err + return p.readMsg(stream) +} + +// shouldRetry checks what error we received and decides whether it is required +// to re-open the connection and retry sending the request. +func (p *dnsOverQUIC) shouldRetry(err error) (ok bool) { + return isQUICRetryError(err) } // getBytesPool returns (creates if needed) a pool we store byte buffers in. func (p *dnsOverQUIC) getBytesPool() (pool *sync.Pool) { - p.Lock() + p.bytesPoolGuard.Lock() + defer p.bytesPoolGuard.Unlock() + if p.bytesPool == nil { p.bytesPool = &sync.Pool{ New: func() interface{} { @@ -154,7 +180,7 @@ func (p *dnsOverQUIC) getBytesPool() (pool *sync.Pool) { }, } } - p.Unlock() + return p.bytesPool } @@ -164,59 +190,57 @@ func (p *dnsOverQUIC) getBytesPool() (pool *sync.Pool) { // close the existing one if needed. func (p *dnsOverQUIC) getConnection(useCached bool) (quic.Connection, error) { var conn quic.Connection - p.RLock() + p.connGuard.RLock() conn = p.conn if conn != nil && useCached { - p.RUnlock() + p.connGuard.RUnlock() + return conn, nil } if conn != nil { // we're recreating the connection, let's create a new one. _ = conn.CloseWithError(QUICCodeNoError, "") } - p.RUnlock() + p.connGuard.RUnlock() - p.Lock() - defer p.Unlock() + p.connGuard.Lock() + defer p.connGuard.Unlock() var err error conn, err = p.openConnection() if err != nil { - // This does not look too nice, but QUIC (or maybe quic-go) doesn't - // seem stable enough. Maybe retransmissions aren't fully implemented - // in quic-go? Anyways, the simple solution is to make a second try when - // it fails to open the QUIC connection. - conn, err = p.openConnection() - if err != nil { - return nil, err - } + return nil, err } p.conn = conn + return conn, nil } +// hasConnection returns true if there's an active QUIC connection. +func (p *dnsOverQUIC) hasConnection() (ok bool) { + p.connGuard.Lock() + defer p.connGuard.Unlock() + + return p.conn != nil +} + // openStream opens a new QUIC stream for the specified connection. func (p *dnsOverQUIC) openStream(conn quic.Connection) (quic.Stream, error) { - ctx := context.Background() - - if p.boot.options.Timeout > 0 { - deadline := time.Now().Add(p.boot.options.Timeout) - var cancel context.CancelFunc - ctx, cancel = context.WithDeadline(context.Background(), deadline) - defer cancel() // avoid resource leak - } + ctx, cancel := p.boot.newContext() + defer cancel() stream, err := conn.OpenStreamSync(ctx) if err == nil { return stream, nil } - // try to recreate the connection. + // We can get here if the old QUIC connection is not valid anymore. We + // should try to re-create the connection again in this case. newConn, err := p.getConnection(false) if err != nil { return nil, err } - // open a new stream. + // Open a new stream. return newConn.OpenStreamSync(ctx) } @@ -244,7 +268,10 @@ func (p *dnsOverQUIC) openConnection() (conn quic.Connection, err error) { addr := udpConn.RemoteAddr().String() - conn, err = quic.DialAddrEarlyContext(context.Background(), addr, tlsConfig, p.quicConfig) + ctx, cancel := p.boot.newContext() + defer cancel() + + conn, err = quic.DialAddrEarlyContext(ctx, addr, tlsConfig, p.quicConfig) if err != nil { return nil, fmt.Errorf("opening quic connection to %s: %w", p.Address(), err) } @@ -256,8 +283,8 @@ func (p *dnsOverQUIC) openConnection() (conn quic.Connection, err error) { // new queries were processed in another connection. We can do that in the case // of a fatal error. func (p *dnsOverQUIC) closeConnWithError(code quic.ApplicationErrorCode) { - p.Lock() - defer p.Unlock() + p.connGuard.Lock() + defer p.connGuard.Unlock() if p.conn == nil { // Do nothing, there's no active conn anyways. @@ -269,6 +296,10 @@ func (p *dnsOverQUIC) closeConnWithError(code quic.ApplicationErrorCode) { log.Error("failed to close the conn: %v", err) } p.conn = nil + + // Re-create the token store to make sure we're not trying to use invalid + // tokens for 0-RTT. + p.quicConfig.TokenStore = newQUICTokenStore() } // readMsg reads the incoming DNS message from the QUIC stream. @@ -297,3 +328,51 @@ func (p *dnsOverQUIC) readMsg(stream quic.Stream) (m *dns.Msg, err error) { return m, nil } + +// newQUICTokenStore creates a new quic.TokenStore that is necessary to have +// in order to benefit from 0-RTT. +func newQUICTokenStore() (s quic.TokenStore) { + // You can read more on address validation here: + // https://datatracker.ietf.org/doc/html/rfc9000#section-8.1 + // Setting maxOrigins to 1 and tokensPerOrigin to 10 assuming that this is + // more than enough for the way we use it (one connection per upstream). + return quic.NewLRUTokenStore(1, 10) +} + +// isQUICRetryError checks the error and determines whether it may signal that +// we should re-create the QUIC connection. This requirement is caused by +// quic-go issues, see the comments inside this function. +// TODO(ameshkov): re-test when updating quic-go. +func isQUICRetryError(err error) (ok bool) { + var qAppErr *quic.ApplicationError + if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { + // This error is often returned when the server has been restarted, + // and we try to use the same connection on the client-side. It seems, + // that the old connections aren't closed immediately on the server-side + // and that's why one can run into this. + // In addition to that, quic-go HTTP3 client implementation does not + // clean up dead connections (this one is specific to DoH3 upstream): + // https://github.com/lucas-clemente/quic-go/issues/765 + return true + } + + var qIdleErr *quic.IdleTimeoutError + if errors.As(err, &qIdleErr) { + // This error means that the connection was closed due to being idle. + // In this case we should forcibly re-create the QUIC connection. + // Reproducing is rather simple, stop the server and wait for 30 seconds + // then try to send another request via the same upstream. + return true + } + + if errors.Is(err, quic.Err0RTTRejected) { + // This error happens when we try to establish a 0-RTT connection with + // a token the server is no more aware of. This can be reproduced by + // restarting the QUIC server (it will clear its tokens cache). The + // next connection attempt will return this error until the client's + // tokens cache is purged. + return true + } + + return false +} diff --git a/upstream/upstream_quic_test.go b/upstream/upstream_quic_test.go index 0a2fe21f..047d19b2 100644 --- a/upstream/upstream_quic_test.go +++ b/upstream/upstream_quic_test.go @@ -1,20 +1,33 @@ package upstream import ( + "context" "crypto/tls" + "encoding/binary" + "fmt" + "io" + "net" + "strconv" "testing" + "time" + "github.com/AdguardTeam/dnsproxy/proxyutil" + "github.com/AdguardTeam/golibs/log" "github.com/lucas-clemente/quic-go" + "github.com/miekg/dns" "github.com/stretchr/testify/require" ) func TestUpstreamDoQ(t *testing.T) { - // Create a DNS-over-QUIC upstream - address := "quic://dns.adguard.com" + srv := startDoQServer(t, 0) + t.Cleanup(srv.Shutdown) + + address := fmt.Sprintf("quic://%s", srv.addr) var lastState tls.ConnectionState u, err := AddressToUpstream( address, &Options{ + InsecureSkipVerify: true, VerifyConnection: func(state tls.ConnectionState) error { lastState = state return nil @@ -47,3 +60,150 @@ func TestUpstreamDoQ(t *testing.T) { // Make sure that the session has been resumed. require.True(t, lastState.DidResume) } + +func TestUpstreamDoQ_serverRestart(t *testing.T) { + // Run the first server instance. + srv := startDoQServer(t, 0) + + // Create a DNS-over-QUIC upstream. + address := fmt.Sprintf("quic://%s", srv.addr) + u, err := AddressToUpstream(address, &Options{InsecureSkipVerify: true, Timeout: time.Second}) + require.NoError(t, err) + + // Test that the upstream works properly. + checkUpstream(t, u, address) + + // Now let's restart the server on the same address. + _, portStr, err := net.SplitHostPort(srv.addr) + require.NoError(t, err) + port, err := strconv.Atoi(portStr) + + // Shutdown the first server. + srv.Shutdown() + + // Start the new one on the same port. + srv = startDoQServer(t, port) + + // Check that everything works after restart. + checkUpstream(t, u, address) + + // Stop the server again. + srv.Shutdown() + + // Now try to send a message and make sure that it returns an error. + _, err = u.Exchange(createTestMessage()) + require.Error(t, err) + + // Start the server one more time. + srv = startDoQServer(t, port) + + // Check that everything works after the second restart. + checkUpstream(t, u, address) +} + +// testDoHServer is an instance of a test DNS-over-QUIC server. +type testDoQServer struct { + // addr is the address that this server listens to. + addr string + + // tlsConfig is the TLS configuration that is used for this server. + tlsConfig *tls.Config + + // listener is the QUIC connections listener. + listener quic.EarlyListener +} + +// Shutdown stops the test server. +func (s *testDoQServer) Shutdown() { + _ = s.listener.Close() +} + +// Serve serves DoQ requests. +func (s *testDoQServer) Serve() { + for { + conn, err := s.listener.Accept(context.Background()) + if err == quic.ErrServerClosed { + // Finish serving on ErrServerClosed error. + return + } + + if err != nil { + log.Debug("error while accepting a new connection: %v", err) + } + + go s.handleQUICConnection(conn) + } +} + +// handleQUICConnection handles incoming QUIC connection. +func (s *testDoQServer) handleQUICConnection(conn quic.EarlyConnection) { + for { + stream, err := conn.AcceptStream(context.Background()) + if err != nil { + _ = conn.CloseWithError(QUICCodeNoError, "") + + return + } + + go func() { + qErr := s.handleQUICStream(stream) + if qErr != nil { + _ = conn.CloseWithError(QUICCodeNoError, "") + } + }() + } +} + +// handleQUICStream handles new QUIC streams, reads DNS messages and responds to +// them. +func (s *testDoQServer) handleQUICStream(stream quic.Stream) (err error) { + defer stream.Close() + + buf := make([]byte, dns.MaxMsgSize+2) + _, err = stream.Read(buf) + if err != nil && err != io.EOF { + return err + } + + req := &dns.Msg{} + packetLen := binary.BigEndian.Uint16(buf[:2]) + err = req.Unpack(buf[2 : packetLen+2]) + if err != nil { + return err + } + + resp := respondToTestMessage(req) + + buf, err = resp.Pack() + if err != nil { + return err + } + + buf = proxyutil.AddPrefix(buf) + _, err = stream.Write(buf) + + return err +} + +// startDoQServer starts a test DoQ server. +func startDoQServer(t *testing.T, port int) (s *testDoQServer) { + tlsConfig := createServerTLSConfig(t, "127.0.0.1") + tlsConfig.NextProtos = []string{NextProtoDQ} + + listen, err := quic.ListenAddrEarly( + fmt.Sprintf("127.0.0.1:%d", port), + tlsConfig, + &quic.Config{}, + ) + require.NoError(t, err) + + s = &testDoQServer{ + addr: listen.Addr().String(), + tlsConfig: tlsConfig, + listener: listen, + } + + go s.Serve() + + return s +} diff --git a/vendor/github.com/bluele/gcache/LICENSE b/vendor/github.com/bluele/gcache/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..d1e7b03e39696805995bfed09c45cc2f28e87086 GIT binary patch literal 1077 zcmWH^NLBFl3{mjOOis-!PF2wG^$gME;&RR}s4U7%&nQvQNY+#^GB7k(@G8wy@XpLF zElT9#3P>%=%`7g?%+FKEELO-!ElN$QR7fvM%qvMv(NRb%N=;SBPg6+FNGwWE)ln$P zS4hmOR47O-D$dVS$WJOs%*@NoOIJu#NX{>)<jPM|D9OkyRw&L-D=AMbN>xbAOHoKH zF3wNROe{%FQAo*8F3nBND@iN?*_W1?lUl5xQIe6WpcD+%p`;16BqcR5hbuEr0VD^L zQYg<X$;dA)Q7B3+E-A`P2AQFwkeQd9Q<?&D157$6GdB}z14ujA@5Njt`3j}QsXAbH z>L}#qr(~v8>L}!<g1uExnv|1SoS~zTk_j?8sk9_jN1+%b2o6dekU#YEixi4eb8@(n z^9wRli$S3RG9KnSuo_VK6o3M>1R6xeAd&Kn{9II@WEOL!l@{e?7H6cUfK;U9D-`F0 zZOlqdE&+*vj84nX$;mGVc`Z3VFC`P?-(o8+E>JutCgqo<g8c&ao<d%JNoF!6xWN$s zb_66=p;E;ei8(n6NvY5POHEP8%;N$H!Msxh@^EoUVqQsRVva&Vei7I{bZ_c$afNue zDg^txg@ii>xhi-DD+C1jhk3fVx+o|)1}k_5E9oeNdxm)UhlVJGI|c<g`h`R)_`4}M z`b8>ud-}QPD7Z!h1i1zWEBFU-dHM$Uc)GgiD0uog`-Hl9`nfAOg@!2j`G+X@c=~#V zxVk8W_$z=cgc{}P8VoYb*EPu5!_hCq(aF=tGbB=n%gr;y4`imBe~^NsLV#mXh^KR? zk7JNRKxj~af3T~9qo0d{pTD1{pIeZppS!ECt6zwof~TK?pTB}@n5$oiLa>LUj}OQy zF2~Ri5C0&L`xKo010sVw-917SJp6rJT!VrYoLm)rJRO~UTou4p`9&%?`#5^~>L|E4 z`Z~IUTo<I^AL8K}#063Yah*cAhbu@3WS^s-f}?YYr@tS_8_xcIAwiDLAvy{n{y`xM z{!p#qp24m<3XVaZ!Jq(f3-b5X;Q|GxzZ*!2r=Nnezn`Bg#1v3agItY>5&s}CKQ!1C Zq#EoN1s7LGA5TB`U<FS<l<3pr0s#G}RyY6v literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/README.md b/vendor/github.com/bluele/gcache/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b8f124b223e31e61269aa713ae33ae192086de49 GIT binary patch literal 5285 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 CSVp!0 literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/arc.go b/vendor/github.com/bluele/gcache/arc.go new file mode 100644 index 0000000000000000000000000000000000000000..e2015e9113490d48b47f13c1e4530b8a68c513e0 GIT binary patch literal 9165 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/cache.go b/vendor/github.com/bluele/gcache/cache.go new file mode 100644 index 0000000000000000000000000000000000000000..e13e6f1cb491407a3c21f2f897bbcd0bdb395216 GIT binary patch literal 5189 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 OrXvMQUS<wgEf)Zq;<j!8 literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/clock.go b/vendor/github.com/bluele/gcache/clock.go new file mode 100644 index 0000000000000000000000000000000000000000..3acc3f0db3ef704823a11884cf8ddde02e324435 GIT binary patch literal 800 zcmXR&OwLYBPgO`yPE5{7<>JcBEyyn_QPAMxR4T5_OIG6IR4U2LO;zI3<l-u+EJ#&w z&dE>CR>;gNNi9lCOiop(=Hm3rFW1mi04dW8$;?gVs)Z^IN=?jxC@(H4Dork_uI1uN zE6qz*@JlU+%V}srjjZP4EJ`gYEy`1X%T?Ec%mQoCC`v|%Ybu~w335e=f|V`A5nxHU zX(b>tz)p8d%ua>42gUhdAucXX$CR?fyyR4k6o@G<rA3J)nfZB|ApKxB_@$P^jnL3k zfU`liL!GXcmYAKI3<{!ZE>2E;eT9(3?9@Dkw4(f6h4jpljM5~%<osOyto*#B{L-A9 zRDH18^8BJ~D}~bHRE0!^w9JZ<(xOy_l8nR>g_Qi%Vuif?5{0zVoV3iG90gGL>UkDN zr55FDXmW9K=H-`LL7kgelB!{7X<?!R;X4)-W#;H8nCK`NAR%xhg@!n5>Tq#_0~Z`H za7QaZA`0Z8y!>*AK_Dx+xHxl5OHwNoKxslRDBKssfrpDmTC#!`+zhOdla{OpHb5`P zCqFqG<n)x(wA3O6gm`EkDAYhQ;D}61*2~K;M~y^qB%oV{E7m}MfSZoPQiPol>lAE3 R4$8|f*K<rs(MSQsJOEA+0iFN= literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/lfu.go b/vendor/github.com/bluele/gcache/lfu.go new file mode 100644 index 0000000000000000000000000000000000000000..9a4e3dfeb6b449233f04377dc3d403c5238d60d9 GIT binary patch literal 8171 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/lru.go b/vendor/github.com/bluele/gcache/lru.go new file mode 100644 index 0000000000000000000000000000000000000000..a85d6603981a92e2e60e7d81a5dc1f5a3b410e25 GIT binary patch literal 6631 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πqX3q*R>;)W2IqcIwFhqLfc1j< zka|vukj5W4dh<$<(;w9DpzuaC{W;NdU}_$?I|1&*)Pj4-9*M;Y$)KK0F{p_GYN>#G zGsOy-d60$-w$_73VlgP)f_wt&18aga5Xj-+R22kER!FHT2vNS|<(Gpz4o=qKraHU{ zkgS&hF<eJMEib<uSHBUe2-yWX3LsM<StTShHx=O`P$XgNe1XeaMQC9Qje2nP2kH($ z0~A!orsiUlzsSWd)Rp@B3PIrF4keuw6y=v?ra&79L?jf55d=aPOW?x87AYhl=@n!a zu3$u{Kn_WykV{iY%_G!tg%|=W<e^Cn8l0de9&%_xf)_Rxg4&coGY2|&0yP($V~~5Q z(0~FpAXD>7iYh_rDHS80xHwZ%b5cuEHJ}|$9gu&DD)q8cD<Opf)JV)OA6`RYU1Eqf z4X|2}nK0wQ1qisR!|v7W)XHKFO@-)Kq#+Js^A1Qa$g|+#7Zek86mnAYU_RB<gpNcg z*eV!sae@jP@aRKPVqSWx0yH>4c7nR;#nG9u3bqQ_puWAfHgbf4Yy;J+`uYm)sU?m+ zKJa#PVonaI#f#Ll1s59l%Rz`48sHKKQjw;B3l2?;F*Pzm29(c>;X#8wGKFLyMnHi| z4l7%5FBeh?K?6(&Jfs6M5;NF9jTRJt)YK?IOD|~I1rA+s)m0pw4GLe712K{)yb%U2 zm0`sWNNF)N;Js5TixI)DP@I#QoT`wY1|HZ1#Sf^g#vkk;6R-t3O7a0YmzcsEY^4=+ z{13$h9R&lTgO;*D1-rpkA+ew!H7^BJ!WHW%WP@4?D1iu41r0-=)I4|)g2F1VG&d== z2o#FokR}|2pph|@=!W!eGV{R0#KeRfMuGt~tfA#3a>fN`KhPjzPHJ9yNd~B7=HkSa zLU9HowxkF#5R}0|%_T%@8X^u2Dd+s$0#K2eQ>l=glbTorPG;bi4YsC0Kxt8Wss^-l zCNMq)u5Xj|3P4)IGK(`y^1-7U$n8{cF$EeA2E__?zj5O47Gdk#pxX#aFggmLo*yI} zK;z|*v2RcqfTseWLz_r50pNi}(CEK{f-`8qjSEz8Lih@xID)l=K*cU-EE_C^ROoPV zBHFaj7Coq$0<uwGUjg2YgQrqZ`btgAgN`(mXQY;7q=F(Nvlvn+Wfnu))Tt>7`9+{H zBv`{U6EyDt8F|)3n5zLwL0DTypn)iqaW2H@IW%=bYHd(hWu}2!S+)oz;0AnV31rw8 dR(OJwD{NpDQKTW+qUV&FmS2>rp#^Q^0RTLWLPG!m literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/simple.go b/vendor/github.com/bluele/gcache/simple.go new file mode 100644 index 0000000000000000000000000000000000000000..7310af141c1ecd25e8b2befd5c8f3e60bce31328 GIT binary patch literal 6402 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 D-%$oW literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/singleflight.go b/vendor/github.com/bluele/gcache/singleflight.go new file mode 100644 index 0000000000000000000000000000000000000000..2c6285e826e0d9a66bc71a61bfd9532f8500fc72 GIT binary patch literal 2117 zcmXR&OwLYBPgO`yPE5{7<>J!U;&RR}s4U7%&nQtaGB7k!aL><A&q-DA%uCke;_}H% zPR%P$O;IS#OGzzKD9K1wa4Z1nRDjCpD1@aJ6=&w>DH!P)C}@BbC_$x^G_AQR^Gg+S z6Dt++@=Fv-i&GU!GBS%5(lT>W6;dmbQwvHIGV>IY^K%PwG86NXQx(cHOESQgK#kDj zii8@FpHz~VnWvDbkepvosgR$BtV$uVgo{f71TsoW3as??%gf9462Z>Y%P&gT&w+TN zSl`Ff+0`%DRo6(*fQu_MFDJFQSfMDjur#wMHANw*QX#RRASW|9F)1fiAt$k1A-_l= zF})}?HASH$ALP*TqRf)aymTFf;{3Fd^2DN4u9VE;lA_F{(h`&qf;wIyvseLHL4KY> zVxEGMW3Yl}u#$q4W3Xqi4p+Emh=+e@h(fqyP>`cvh^K3?f`5>Lv%jB<XNaf2U$BC| zn}VZXq=L7npNoz{YGz4BYLP-}ML`kB?fFFtnV_&sP0`~DP6dT-DlA?U(()nERa}so zoSBxHtdNtKmtG2v`uwugqP)z!bcKS{qTI}4Q0x^eB<7`X<z(h&mL!&B=I0e7ML5(u zdR$uiTwMD43L&64%*{_J%}G@#D9SI(Oi3+PNK{BE1qWV9szO?6UNXoKh2+GX9EIZ2 z0<dc{^YcKub5oNu67w>PbM?5mGII;^i%JxfiYxPymB40$b!38EqmY@Wo0bDkmia}H zG?EGmW|w@hv0NpU1*u?`LUBn^X>y4|H5X@jx`G18EWPl=%o6va{L%t0&a%WDh0MH? z)S|S+<kad~F3!}VB8AkVqWmJRTCkO1bqYnPAV;R=m4LjGoRe5w3`%6>`9;~FkO75e zE=VRXF*miiATc==oE|bVlQTemD$UC*L8wno%u`4LCGgZ_aKZp5YDDNDg@+!*7Z4X9 zyaCRd3eKQx#>JUi3QE{uU-_1nq*j1<Fs!c+ihz>T<dR~ATrSRBuy}4_K{S$wW3|9x zs;{q*lUS9RQ>l=dmsygTn3GwRngR_0mwbiP3b12A0iU8!lv+|+lvfPSVnwONr8yAK zf%0~GW?5<;Jc4x;auc&N^U^_KP+VFBN`Q$a3i)|Cl?wTJun+*14VlGA(FsbKi3$*t zvQsM+5=#^kK_--B=BDZ?c&4GGrR4nFRFGyJuxk*Z2=Y{FkwSSQDC9vVmSm)Y49PFb zOwY_q%mHOtaKVyVk_wK&qSWNn%(7I7uZk0MQx%|I)8hiAR|So91ubwyXezkmYk=H^ z6o@(sX%Js)Xewx+h=Ws-rj9~pF(}O`B<1Jlpeq51=|Ga4CMcDr>*bc}`Q#^OYiM$D zmVspytZWt1^}zA2m!4VzvR+4_q^LAilZ!Jm4Ww7W)>a`eGY6!f6Kp_eUJld<&LVJp zDU|6bq$TDQr|N(da&gvzwWjOkqG?sI1;tvb2G(?<3DcLXqmZ8saaL}0c4}p;HHcr$ z#ff2iW}1Q`)b}81m}~MfbKvgrPObFIFLBE+%}a3w=R8i3>o}A3%G1GF6%-IKeaU)d zi3pv^dZ|T4U^gX$9Fvz?t^ta9O)iLL$CMNeLrpG_o4}4#uvJI~^Gfv);e+PD^n7qo z=j3Q4>nLQWR)XSIlZz8(KYn-D!ea~)Vd$nHq5>3q;J5{4GV~M%@`FaQ0yt}e9f4d# zfgA+RVmJ~TdisJSK~Tzoh8;N26l@jJ@<0I#4rQ18yi^TME@;@m5?@McPHIW2M!H@u U*b$I`L<%}CSfC&q%vH+;04ox;y#N3J literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/stats.go b/vendor/github.com/bluele/gcache/stats.go new file mode 100644 index 0000000000000000000000000000000000000000..ca0bf3185ae7e1da68114d2ba4cc3543cbbadf4f GIT binary patch literal 1003 zcmXR&OwLYBPgO`yPE5{7<>JcBEyyn_QPAMxR4T5_OV&>;$<NJ9R^rm+;wq^uNL45< zNh~RLOioTMF3vAf$jmEAElNvFPF1Mp;`GQYan3KzE78zYD9y|(F*D)f^vx_TMi=wR z&(AI`Ko|AMED1_1N!8F)NXyAj1Zl42;?ma#+m~5fl9^nLWWPdjNl|HX3CPk6sHF;U zJ9A-n!bBhjW#%OprRJvQl_+FnmMA2H9LkkenwPAgQCy;+1@@Mvf@fZG5r#KF))b|d zlosVFKte;$F(oAw>_ZK;;u1ZW6Lb^|HM!6&1GxsDZI~fM$U2aT;Lt;|59VeCh&zk% z`4OfT9!;1Q`s62K^B~N3a5uxP1Gx#8FX5_*w-96;*o!b5k<tv@&Kz($!euLx4s7-o zm*^pdgSJ9(i5`+Wp|J=}lW-f05=&AQ((;QGz<C7}#wD3~>6obqst=KtK_Q%xtfP>d ztYBpevI}9i4v33vHD^hFNn#F2T}HBkwnA<)7iVUg0z}Z(R>1&d7AGW{6%6zYxHxOM XU|eX<(a1>FRM3azBMq=tP(T6zT?0x^ literal 0 HcmV?d00001 diff --git a/vendor/github.com/bluele/gcache/utils.go b/vendor/github.com/bluele/gcache/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..1f784e4c4cf3a5e8715c79ce817b5567934c2200 GIT binary patch literal 149 zcmXR&OwLYBPgO`yPE5{7<>E>!%}Z9u&CK)6E77RXQK(eN%q!6Z5en5@oSA6~6$&;A jl^`}}QEEwPQJz8t7iTROj8n-~3o|0I0=E%%1dIRxt=KJe literal 0 HcmV?d00001 diff --git a/vendor/modules.txt b/vendor/modules.txt index 170d03040e5282e5a3f9ca4ddfd1e9471e212b05..9671387919c21adc6bffcb9e9c3d577e26398819 100644 GIT binary patch delta 54 zcmdmD++wo9npq?%r!+MuRX;sBF*zeuq0B(fK+kCMLJ^V4|5$i=kR><kFn5Rn0RLqY AQ2+n{ delta 12 TcmZoM*<!rGnt5|KbEOynA2tMD -- GitLab