diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ccc01397dd2d426de172e3d68595a8e0a7743f1f..12ae895a70bbfd66ab853c5984015d48f587cd99 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -29,7 +29,7 @@ jobs: with: # This field is required. Dont set the patch version to always use # the latest patch version. - version: v1.45.1 + version: v1.48.0 notify: needs: - golangci diff --git a/README.md b/README.md index 50c963ee06898fe6f8139015339b23b8db6b27c3..ff36c26c68473024c6e59640d284116f581d540d 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ Usage: dnsproxy [OPTIONS] Application Options: - --config-path= yaml configuration file. Minimal working configuration in config.yaml.dist. Options passed - through command line will override the ones from this file. + --config-path= yaml configuration file. Minimal working configuration in config.yaml.dist. + Options passed through command line will override the ones from this file. -v, --verbose Verbose output (optional) -o, --output= Path to the log file. If not set, write to stdout. -l, --listen= Listening addresses @@ -52,10 +52,10 @@ Application Options: --tls-min-version= Minimum TLS version, for example 1.0 --tls-max-version= Maximum TLS version, for example 1.3 --insecure Disable secure TLS certificate validation - -g, --dnscrypt-config= Path to a file with DNSCrypt configuration. You can generate one using - https://github.com/ameshkov/dnscrypt - -u, --upstream= An upstream to be used (can be specified multiple times). You can also specify path to a file - with the list of servers + -g, --dnscrypt-config= Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt + --http3 Enable HTTP/3 support + -u, --upstream= An upstream to be used (can be specified multiple times). + You can also specify path to a file with the list of servers -b, --bootstrap= Bootstrap DNS for DoH and DoT, can be specified multiple times (default: 8.8.8.8:53) -f, --fallback= Fallback resolvers to use when regular ones are unavailable, can be specified multiple times. You can also specify path to a file with the list of servers @@ -63,8 +63,8 @@ Application Options: --fastest-addr Respond to A or AAAA requests only with the fastest IP address --cache If specified, DNS cache is enabled --cache-size= Cache size (in bytes). Default: 64k - --cache-min-ttl= Minimum TTL value for DNS entries, in seconds. Capped at 3600. Artificially extending TTLs - should only be done with careful consideration. + --cache-min-ttl= Minimum TTL value for DNS entries, in seconds. Capped at 3600. + Artificially extending TTLs should only be done with careful consideration. --cache-max-ttl= Maximum TTL value for DNS entries, in seconds. --cache-optimistic If specified, optimistic DNS cache is enabled -r, --ratelimit= Ratelimit (requests per second) @@ -75,8 +75,8 @@ Application Options: --dns64-prefix= If specified, this is the DNS64 prefix dnsproxy will be using when it works as a DNS64 server. If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b:: --ipv6-disabled If specified, all AAAA requests will be replied with NoError RCode and empty answer - --bogus-nxdomain= Transform the responses containing at least a single IP that matches specified addresses and - CIDRs into NXDOMAIN. Can be specified multiple times. + --bogus-nxdomain= Transform the responses containing at least a single IP that matches specified addresses + and CIDRs into NXDOMAIN. Can be specified multiple times. --udp-buf-size= Set the size of the UDP buffer in bytes. A value <= 0 will use the system default. --max-go-routines= Set the maximum number of go routines. A value <= 0 will not not set a maximum. --version Prints the program version @@ -144,6 +144,16 @@ DNS-over-QUIC upstream: ./dnsproxy -u quic://dns.adguard.com ``` +DNS-over-HTTPS upstream with enabled HTTP/3 support (chooses it if it's faster): +```shell +./dnsproxy -u https://dns.google/dns-query --http3 +``` + +DNS-over-HTTPS upstream with forced HTTP/3 (no fallback to other protocol): +```shell +./dnsproxy -u h3://dns.google/dns-query +``` + DNSCrypt upstream ([DNS Stamp](https://dnscrypt.info/stamps) of AdGuard DNS): ```shell ./dnsproxy -u sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20 diff --git a/go.mod b/go.mod index a011f4cb371289034fd36ce1a170af3fcf6b935e..52232f05891a41bc4a97f9cd0c79327775cb3aa8 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/kr/text v0.2.0 // indirect + github.com/marten-seemann/qpack v0.2.1 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect diff --git a/go.sum b/go.sum index be7c9b390204abb4b0b228ae361338fd6931ef73..484a8304ef8c7f6719adab2275539883e1dd2953 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE= github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE= +github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU= @@ -55,6 +57,7 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= @@ -87,6 +90,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -104,6 +108,7 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -117,6 +122,7 @@ golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVq golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= diff --git a/main.go b/main.go index ecc3bdd4c19ed0aa36dda33fec48d249fc89dc61..df92091f1fa75ae52a6838e177d1806f19d2f830 100644 --- a/main.go +++ b/main.go @@ -80,6 +80,11 @@ type Options struct { // Path to the DNSCrypt configuration file 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. + HTTP3 bool `yaml:"http3" long:"http3" description:"Enable HTTP/3 support" optional:"yes" optional-value:"false"` + // Upstream DNS servers settings // -- @@ -292,7 +297,18 @@ func createProxyConfig(options *Options) proxy.Config { func initUpstreams(config *proxy.Config, options *Options) { // Init upstreams upstreams := loadServersList(options.Upstreams) + + httpVersions := upstream.DefaultHTTPVersions + if options.HTTP3 { + httpVersions = []upstream.HTTPVersion{ + upstream.HTTPVersion3, + upstream.HTTPVersion2, + upstream.HTTPVersion11, + } + } + upsOpts := &upstream.Options{ + HTTPVersions: httpVersions, InsecureSkipVerify: options.Insecure, Bootstrap: options.BootstrapDNS, Timeout: defaultTimeout, diff --git a/proxy/errors.go b/proxy/errors.go index 88089c92c466caf8423de995b98e4708630ea097..241fec6b68151151e2cabf98ae3a8ebe9f13a9e0 100644 --- a/proxy/errors.go +++ b/proxy/errors.go @@ -12,11 +12,11 @@ import ( // isEPIPE checks if the underlying error is EPIPE. syscall.EPIPE exists on all // OSes except for Plan 9. Validate with: // -// $ for os in $(go tool dist list | cut -d / -f 1 | sort -u) -// do -// echo -n "$os" -// env GOOS="$os" go doc syscall.EPIPE | grep -F -e EPIPE -// done +// $ for os in $(go tool dist list | cut -d / -f 1 | sort -u) +// do +// echo -n "$os" +// env GOOS="$os" go doc syscall.EPIPE | grep -F -e EPIPE +// done // // For the Plan 9 version see ./errors_plan9.go. func isEPIPE(err error) (ok bool) { diff --git a/proxy/server_https.go b/proxy/server_https.go index 9fc15536dc80852e46dbaaa18791830b69c69516..7324fc14eaf3aa2f900e7eaa0156f9f332c6e89e 100644 --- a/proxy/server_https.go +++ b/proxy/server_https.go @@ -56,11 +56,10 @@ func (p *Proxy) listenHTTPS(srv *http.Server, l net.Listener) { // ServeHTTP is the http.RequestHandler 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. -// +// - 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) { log.Tracef("Incoming HTTPS request on %s", r.URL) @@ -159,11 +158,10 @@ func (p *Proxy) respondHTTPS(d *DNSContext) error { // suitable r's header. It returns nil if r doesn't contain any information // about real client's IP address. Current headers priority is: // -// 1. CF-Connecting-IP -// 2. True-Client-IP -// 3. X-Real-IP -// 4. X-Forwarded-For -// +// 1. CF-Connecting-IP +// 2. True-Client-IP +// 3. X-Real-IP +// 4. X-Forwarded-For func realIPFromHdrs(r *http.Request) (realIP net.IP) { for _, h := range []string{ // Headers set by CloudFlare proxy servers. diff --git a/proxy/upstreams.go b/proxy/upstreams.go index a57b240f2bb3c4f1357babe16b75cf61444c82a4..f85255a5e29589f82d6c1362193a152658a766fc 100644 --- a/proxy/upstreams.go +++ b/proxy/upstreams.go @@ -72,13 +72,7 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (* dnsUpstream, ok := upstreamsIndex[u] if !ok { // create an upstream - dnsUpstream, err = upstream.AddressToUpstream( - u, - &upstream.Options{ - Bootstrap: options.Bootstrap, - Timeout: options.Timeout, - InsecureSkipVerify: options.InsecureSkipVerify, - }) + dnsUpstream, err = upstream.AddressToUpstream(u, options.Clone()) if err != nil { err = fmt.Errorf("cannot prepare the upstream %s (%s): %s", l, options.Bootstrap, err) diff --git a/proxyutil/udp_unix.go b/proxyutil/udp_unix.go index b5e625c377962537c4a9a84021a4a55682fb6a8f..e9d13dab8e4bd9bb9c971d1cd126ef25e67acd16 100644 --- a/proxyutil/udp_unix.go +++ b/proxyutil/udp_unix.go @@ -15,9 +15,8 @@ import ( // connection to receive an appropriate OOB data. For both versions the flags // are: // -// FlagDst -// FlagInterface -// +// - FlagDst +// - FlagInterface const ( ipv4Flags ipv4.ControlFlags = ipv4.FlagDst | ipv4.FlagInterface ipv6Flags ipv6.ControlFlags = ipv6.FlagDst | ipv6.FlagInterface diff --git a/upstream/bootstrap.go b/upstream/bootstrap.go index a78d69391f215d5ec55652dca60cdc236ae063f6..a5e70edb5518aa42b339d977eed7b356a11eb8e9 100755 --- a/upstream/bootstrap.go +++ b/upstream/bootstrap.go @@ -12,53 +12,66 @@ import ( "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" - "golang.org/x/net/http2" ) -// NextProtoDQ is the ALPN token for DoQ. During connection establishment, -// DNS/QUIC support is indicated by selecting the ALPN token "dq" in the +// NextProtoDQ is the ALPN token for DoQ. During the connection establishment, +// DNS/QUIC support is indicated by selecting the ALPN token "doq" in the // crypto handshake. -// Current draft version: -// https://datatracker.ietf.org/doc/html/draft-ietf-dprive-dnsoquic-02 -const NextProtoDQ = "doq-i02" +// The current draft version is https://datatracker.ietf.org/doc/rfc9250/. +const NextProtoDQ = "doq" // compatProtoDQ is a list of ALPN tokens used by a QUIC connection. // NextProtoDQ is the latest draft version supported by dnsproxy, but it also // includes previous drafts. -var compatProtoDQ = []string{NextProtoDQ, "doq-i00", "dq", "doq"} +var compatProtoDQ = []string{NextProtoDQ, "doq-i00", "dq", "doq-i02"} -// RootCAs is the CertPool that must be used by all upstreams -// Redefining RootCAs makes sense on iOS to overcome the 15MB memory limit of the NEPacketTunnelProvider -// nolint +// RootCAs is the CertPool that must be used by all upstreams. Redefining +// RootCAs makes sense on iOS to overcome the 15MB memory limit of the +// NEPacketTunnelProvider. var RootCAs *x509.CertPool -// CipherSuites - custom list of TLSv1.2 ciphers -// nolint +// CipherSuites is a custom list of TLSv1.2 ciphers. var CipherSuites []uint16 -// TODO: refactor bootstrapper, it's overcomplicated and hard to understand what it does +// TODO(ameshkov): refactor bootstrapper, it's overcomplicated and hard to +// understand what it does. type bootstrapper struct { - URL *url.URL - resolvers []*Resolver // list of Resolvers to use to resolve hostname, if necessary - dialContext dialHandler // specifies the dial function for creating unencrypted TCP connections. + // URL is the upstream server address. + URL *url.URL + + // resolvers is a list of *net.Resolver to use to resolve the upstream + // hostname, if necessary. + resolvers []*Resolver + + // dialContext is the dial function for creating unencrypted TCP + // connections. + dialContext dialHandler + + // resolvedConfig is a *tls.Config that is used for encrypted DNS protocols. resolvedConfig *tls.Config - sync.RWMutex - // stores options for AddressToUpstream func: - // callbacks for checking certificates, timeout, - // the need to verify the server certificate, - // the addresses of upstream servers, etc + // sessionsCache is necessary to achieve TLS session resumption. We create + // once when the bootstrapper is created and re-use every time when we need + // to create a new tls.Config. + sessionsCache tls.ClientSessionCache + + // guard protects dialContext and resolvedConfig. + guard sync.RWMutex + + // options is the Options that were passed to the AddressToUpstream + // function. It configures different upstream properties: callbacks for + // checking certificates, timeout, etc. options *Options } -// newBootstrapperResolved creates a new bootstrapper that already contains resolved config. -// This can be done only in the case when we already know the resolver IP address. -// options -- Upstream customization options +// newBootstrapperResolved creates a new bootstrapper that already contains +// resolved config. This can be done only in the case when we already know the +// resolver IP address passed via options. func newBootstrapperResolved(upsURL *url.URL, options *Options) (*bootstrapper, error) { // get a host without port host, port, err := net.SplitHostPort(upsURL.Host) if err != nil { - return nil, fmt.Errorf("bootstrapper requires port in address %s", upsURL.String()) + return nil, fmt.Errorf("bootstrapper requires port in address %s", upsURL) } var resolverAddresses []string @@ -70,6 +83,10 @@ func newBootstrapperResolved(upsURL *url.URL, options *Options) (*bootstrapper, b := &bootstrapper{ URL: upsURL, options: options, + // Use the default capacity for the LRU cache. It may be useful to + // store several caches since the user may be routed to different + // servers in case there's load balancing on the server-side. + sessionsCache: tls.NewLRUClientSessionCache(0), } b.dialContext = b.createDialContext(resolverAddresses) b.resolvedConfig = b.createTLSConfig(host) @@ -77,9 +94,9 @@ func newBootstrapperResolved(upsURL *url.URL, options *Options) (*bootstrapper, return b, nil } -// newBootstrapper initializes a new bootstrapper instance -// address -- original resolver address string (i.e. tls://one.one.one.one:853) -// options -- Upstream customization options +// newBootstrapper initializes a new bootstrapper instance. u is the original +// resolver address string (i.e. tls://one.one.one.one:853), options is the +// upstream configuration options. func newBootstrapper(u *url.URL, options *Options) (b *bootstrapper, err error) { resolvers := []*Resolver{} if len(options.Bootstrap) != 0 { @@ -103,18 +120,31 @@ func newBootstrapper(u *url.URL, options *Options) (b *bootstrapper, err error) URL: u, resolvers: resolvers, options: options, + // Use the default capacity for the LRU cache. It may be useful to + // store several caches since the user may be routed to different + // servers in case there's load balancing on the server-side. + sessionsCache: tls.NewLRUClientSessionCache(0), }, nil } -// dialHandler specifies the dial function for creating unencrypted TCP connections. +// dialHandler describes the dial function for creating unencrypted network +// connections to the upstream server. Internally, this function will use the +// supplied bootstrap DNS servers to resolve the upstream's IP address and only +// then it will actually establish a connection. type dialHandler func(ctx context.Context, network, addr string) (net.Conn, error) -// will get usable IP address from Address field, and caches the result +// get is the main function of bootstrapper that does two crucial things. +// First, it creates an instance of a dialHandler function that should be used +// by the Upstream to establish a connection to the upstream DNS server. This +// dialHandler in a lazy manner resolves the DNS server IP address using the +// bootstrap DNS servers supplied to this bootstrapper instance. It will also +// create an instance of *tls.Config that should be used for establishing an +// encrypted connection for DoH/DoT/DoQ. func (n *bootstrapper) get() (*tls.Config, dialHandler, error) { - n.RLock() + n.guard.RLock() if n.dialContext != nil && n.resolvedConfig != nil { // fast path tlsConfig, dialContext := n.resolvedConfig, n.dialContext - n.RUnlock() + n.guard.RUnlock() return tlsConfig.Clone(), dialContext, nil } @@ -123,22 +153,24 @@ func (n *bootstrapper) get() (*tls.Config, dialHandler, error) { // // get a host without port - addr := n.URL - host, port, err := net.SplitHostPort(addr.Host) + u := n.URL + host, port, err := net.SplitHostPort(u.Host) if err != nil { - n.RUnlock() - return nil, nil, fmt.Errorf("bootstrapper requires port in address %s", addr.String()) + n.guard.RUnlock() + return nil, nil, fmt.Errorf("bootstrapper requires port in address %s", u) } - // if n.address's host is an IP, just use it right away + // if n.address's host is an IP, just use it right away. ip := net.ParseIP(host) if ip != nil { - n.RUnlock() + n.guard.RUnlock() - // Upgrade lock to protect n.resolved resolverAddress := net.JoinHostPort(host, port) - n.Lock() - defer n.Unlock() + + // Upgrade lock to protect n.resolvedConfig. + // TODO(ameshkov): rework, that's not how it should be done. + n.guard.Lock() + defer n.guard.Unlock() n.dialContext = n.createDialContext([]string{resolverAddress}) n.resolvedConfig = n.createTLSConfig(host) @@ -148,7 +180,7 @@ func (n *bootstrapper) get() (*tls.Config, dialHandler, error) { // Don't lock anymore (we can launch multiple lookup requests at a time) // Otherwise, it might mess with the timeout specified for the Upstream // See here: https://github.com/AdguardTeam/dnsproxy/issues/15 - n.RUnlock() + n.guard.RUnlock() // // if it's a hostname @@ -182,24 +214,26 @@ func (n *bootstrapper) get() (*tls.Config, dialHandler, error) { return nil, nil, fmt.Errorf("couldn't find any suitable IP address for host %s", host) } - n.Lock() - defer n.Unlock() + n.guard.Lock() + defer n.guard.Unlock() n.dialContext = n.createDialContext(resolved) n.resolvedConfig = n.createTLSConfig(host) return n.resolvedConfig, n.dialContext, nil } -// createTLSConfig creates a client TLS config +// createTLSConfig creates a client TLS config that will be used to establish +// an encrypted connection for DoH/DoT/DoQ. func (n *bootstrapper) createTLSConfig(host string) *tls.Config { tlsConfig := &tls.Config{ ServerName: host, RootCAs: RootCAs, CipherSuites: CipherSuites, + ClientSessionCache: n.sessionsCache, MinVersion: tls.VersionTLS12, InsecureSkipVerify: n.options.InsecureSkipVerify, VerifyPeerCertificate: n.options.VerifyServerCertificate, - ClientSessionCache: tls.NewLRUClientSessionCache(1), + VerifyConnection: n.options.VerifyConnection, } // Depending on the URL scheme, we choose what ALPN will be advertised by @@ -210,16 +244,26 @@ func (n *bootstrapper) createTLSConfig(host string) *tls.Config { // // See https://github.com/ameshkov/dnslookup/issues/19. case "https": - tlsConfig.NextProtos = []string{http2.NextProtoTLS, "http/1.1"} + httpVersions := n.options.HTTPVersions + if httpVersions == nil { + httpVersions = DefaultHTTPVersions + } + + var nextProtos []string + for _, v := range httpVersions { + nextProtos = append(nextProtos, string(v)) + } + + tlsConfig.NextProtos = nextProtos case "quic": tlsConfig.NextProtos = compatProtoDQ - tlsConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10) } return tlsConfig } -// createDialContext returns dialContext function that tries to establish connection with all given addresses one by one +// createDialContext returns a dialHandler function that tries to establish the +// connection to each of the provided addresses one by one. func (n *bootstrapper) createDialContext(addresses []string) (dialContext dialHandler) { dialer := &net.Dialer{ Timeout: n.options.Timeout, @@ -240,14 +284,23 @@ func (n *bootstrapper) createDialContext(addresses []string) (dialContext dialHa conn, err := dialer.DialContext(ctx, network, resolverAddress) elapsed := time.Since(start) if err == nil { - log.Tracef("dialer has successfully initialized connection to %s in %s", resolverAddress, elapsed) + log.Tracef( + "dialer has successfully initialized connection to %s in %s", + resolverAddress, + elapsed, + ) return conn, nil } errs = append(errs, err) - log.Tracef("dialer failed to initialize connection to %s, in %s, cause: %s", resolverAddress, elapsed, err) + log.Tracef( + "dialer failed to initialize connection to %s, in %s, cause: %s", + resolverAddress, + elapsed, + err, + ) } return nil, errors.List("all dialers failed", errs...) diff --git a/upstream/upstream.go b/upstream/upstream.go index 235fcbf9e02f9b1ce31959bbf730908a875f5366..586526d6774abe96d9ededb18f6f3206d1bf2234 100644 --- a/upstream/upstream.go +++ b/upstream/upstream.go @@ -1,7 +1,9 @@ -// Package upstream implements DNS clients for all known DNS encryption protocols +// Package upstream implements DNS clients for all known DNS encryption +// protocols. package upstream import ( + "crypto/tls" "crypto/x509" "fmt" "net" @@ -17,13 +19,17 @@ import ( "github.com/miekg/dns" ) -// Upstream is an interface for a DNS resolver +// Upstream is an interface for a DNS resolver. type Upstream interface { + // Exchange sends the DNS query m to this upstream and returns the response + // that has been received or an error if something went wrong. Exchange(m *dns.Msg) (*dns.Msg, error) + // Address returns the address of the upstream DNS resolver. Address() string } -// Options for AddressToUpstream func +// Options for AddressToUpstream func. With these options we can configure the +// upstream properties. type Options struct { // Bootstrap is a list of DNS servers to be used to resolve // DNS-over-HTTPS/DNS-over-TLS hostnames. Plain DNS, DNSCrypt, or @@ -42,17 +48,55 @@ type Options struct { // InsecureSkipVerify disables verifying the server's certificate. InsecureSkipVerify bool - // VerifyServerCertificate used to be set to crypto/tls - // Config.VerifyPeerCertificate for DNS-over-HTTPS, DNS-over-QUIC, - // DNS-over-TLS. + // HTTPVersions is a list of HTTP versions that should be supported by the + // DNS-over-HTTPS client. If not set, HTTP/1.1 and HTTP/2 will be used. + HTTPVersions []HTTPVersion + + // VerifyServerCertificate is used to set the VerifyPeerCertificate property + // of the *tls.Config for DNS-over-HTTPS, DNS-over-QUIC, and DNS-over-TLS. VerifyServerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error + // VerifyConnection is used to set the VerifyConnection property + // of the *tls.Config for DNS-over-HTTPS, DNS-over-QUIC, and DNS-over-TLS. + VerifyConnection func(state tls.ConnectionState) error + // VerifyDNSCryptCertificate is the callback the DNSCrypt server certificate // will be passed to. It's called in dnsCrypt.exchangeDNSCrypt. // Upstream.Exchange method returns any error caused by it. VerifyDNSCryptCertificate func(cert *dnscrypt.Cert) error } +// Clone copies o to a new struct. Note, that this is not a deep clone. +func (o *Options) Clone() (clone *Options) { + return &Options{ + Bootstrap: o.Bootstrap, + Timeout: o.Timeout, + ServerIPAddrs: o.ServerIPAddrs, + InsecureSkipVerify: o.InsecureSkipVerify, + HTTPVersions: o.HTTPVersions, + VerifyServerCertificate: o.VerifyServerCertificate, + VerifyConnection: o.VerifyConnection, + VerifyDNSCryptCertificate: o.VerifyDNSCryptCertificate, + } +} + +// HTTPVersion is an enumeration of the HTTP versions that we support. Values +// that we use in this enumeration are also used as ALPN values. +type HTTPVersion string + +const ( + // HTTPVersion11 is HTTP/1.1. + HTTPVersion11 HTTPVersion = "http/1.1" + // HTTPVersion2 is HTTP/2. + HTTPVersion2 HTTPVersion = "h2" + // HTTPVersion3 is HTTP/3. + HTTPVersion3 HTTPVersion = "h3" +) + +// DefaultHTTPVersions is the list of HTTPVersion that we use by default in +// the DNS-over-HTTPS client. +var DefaultHTTPVersions = []HTTPVersion{HTTPVersion11, HTTPVersion2} + const ( // defaultPortPlain is the default port for plain DNS. defaultPortPlain = 53 @@ -72,11 +116,12 @@ const ( // AddressToUpstream converts addr to an Upstream instance: // -// 8.8.8.8:53 or udp://dns.adguard.com for plain DNS; -// tcp://8.8.8.8:53 for plain DNS-over-TCP; -// tls://1.1.1.1 for DNS-over-TLS; -// https://dns.adguard.com/dns-query for DNS-over-HTTPS; -// sdns://... for DNS stamp, see https://dnscrypt.info/stamps-specifications. +// - 8.8.8.8:53 or udp://dns.adguard.com for plain DNS; +// - tcp://8.8.8.8:53 for plain DNS-over-TCP; +// - tls://1.1.1.1 for DNS-over-TLS; +// - https://dns.adguard.com/dns-query for DNS-over-HTTPS; +// - h3://dns.google for DNS-over-HTTPS that only works with HTTP/3; +// - sdns://... for DNS stamp, see https://dnscrypt.info/stamps-specifications. // // opts are applied to the u. nil is a valid value for opts. func AddressToUpstream(addr string, opts *Options) (u Upstream, err error) { @@ -129,6 +174,10 @@ func urlToUpstream(uu *url.URL, opts *Options) (u Upstream, err error) { return newDoQ(uu, opts) case "tls": return newDoT(uu, opts) + case "h3": + opts.HTTPVersions = []HTTPVersion{HTTPVersion3} + uu.Scheme = "https" + return newDoH(uu, opts) case "https": return newDoH(uu, opts) default: @@ -190,11 +239,10 @@ func logBegin(upstreamAddress string, req *dns.Msg) { qtype := "" target := "" if len(req.Question) != 0 { - qtype = dns.TypeToString[req.Question[0].Qtype] + qtype = dns.Type(req.Question[0].Qtype).String() target = req.Question[0].Name } - log.Debug("%s: sending request %s %s", - upstreamAddress, qtype, target) + log.Debug("%s: sending request %s %s", upstreamAddress, qtype, target) } // Write to log about the result of DNS request @@ -203,6 +251,5 @@ func logFinish(upstreamAddress string, err error) { if err != nil { status = err.Error() } - log.Debug("%s: response: %s", - upstreamAddress, status) + log.Debug("%s: response: %s", upstreamAddress, status) } diff --git a/upstream/upstream_dnscrypt.go b/upstream/upstream_dnscrypt.go index e3c05a837213c2919a6902708002ac6f9952539e..d0a0c04d59946a09544156f8ac53625429bef025 100644 --- a/upstream/upstream_dnscrypt.go +++ b/upstream/upstream_dnscrypt.go @@ -13,9 +13,8 @@ import ( "github.com/miekg/dns" ) -// -// DNSCrypt -// +// dnsCrypt is a struct that implements the Upstream interface for the DNSCrypt +// protocol. type dnsCrypt struct { boot *bootstrapper client *dnscrypt.Client // DNSCrypt client properties @@ -27,8 +26,10 @@ type dnsCrypt struct { // type check var _ Upstream = (*dnsCrypt)(nil) +// Address implements the Upstream interface for *dnsCrypt. func (p *dnsCrypt) Address() string { return p.boot.URL.String() } +// Exchange implements the Upstream interface for *dnsCrypt. func (p *dnsCrypt) Exchange(m *dns.Msg) (*dns.Msg, error) { reply, err := p.exchangeDNSCrypt(m) diff --git a/upstream/upstream_doh.go b/upstream/upstream_doh.go index b71e64e9d65889edf7d8ef6b4586fba911b8d289..78a93afa9eb6168ef34a188188bea0b70c0f70f5 100644 --- a/upstream/upstream_doh.go +++ b/upstream/upstream_doh.go @@ -1,9 +1,12 @@ package upstream import ( + "context" + "crypto/tls" "encoding/base64" "fmt" "io" + "net" "net/http" "net/url" "os" @@ -11,6 +14,9 @@ import ( "time" "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/log" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" "github.com/miekg/dns" "golang.org/x/net/http2" ) @@ -34,7 +40,8 @@ const ( dohMaxIdleConns = 1 ) -// dnsOverHTTPS represents DNS-over-HTTPS upstream. +// dnsOverHTTPS is a struct that implements the Upstream interface for the +// DNS-over-HTTPS protocol. type dnsOverHTTPS struct { boot *bootstrapper @@ -43,6 +50,10 @@ type dnsOverHTTPS struct { // needed. Clients are safe for concurrent use by multiple goroutines. client *http.Client clientGuard sync.Mutex + + // quicConfig is the QUIC configuration that is used if HTTP/3 is enabled + // for this upstream. + quicConfig *quic.Config } // type check @@ -58,11 +69,25 @@ func newDoH(uu *url.URL, opts *Options) (u Upstream, err error) { return nil, fmt.Errorf("creating https bootstrapper: %w", err) } - return &dnsOverHTTPS{boot: b}, nil + return &dnsOverHTTPS{ + 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), + }, + }, nil } +// Address implements the Upstream interface for *dnsOverHTTPS. 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) { client, err := p.getClient() if err != nil { @@ -146,8 +171,8 @@ func (p *dnsOverHTTPS) getClient() (c *http.Client, err error) { return p.client, nil } - // Timeout can be exceeded while waiting for the lock - // This happens quite often on mobile devices + // Timeout can be exceeded while waiting for the lock. This happens quite + // often on mobile devices. elapsed := time.Since(startTime) if p.boot.options.Timeout > 0 && elapsed > p.boot.options.Timeout { return nil, fmt.Errorf("timeout exceeded: %s", elapsed) @@ -158,6 +183,10 @@ func (p *dnsOverHTTPS) getClient() (c *http.Client, err error) { return p.client, err } +// createClient creates a new *http.Client instance. The HTTP protocol version +// will depend on whether HTTP3 is allowed and provided by this upstream. Note, +// that we'll attempt to establish a QUIC connection when creating the client in +// order to check whether HTTP3 is supported. func (p *dnsOverHTTPS) createClient() (*http.Client, error) { transport, err := p.createTransport() if err != nil { @@ -175,14 +204,32 @@ func (p *dnsOverHTTPS) createClient() (*http.Client, error) { } // createTransport initializes an HTTP transport that will be used specifically -// for this DoH resolver. This HTTP transport ensures that the HTTP requests -// will be sent exactly to the IP address got from the bootstrap resolver. -func (p *dnsOverHTTPS) createTransport() (*http.Transport, error) { +// for this DoH resolver. This HTTP transport ensures that the HTTP requests +// will be sent exactly to the IP address got from the bootstrap resolver. Note, +// that this function will first attempt to establish a QUIC connection (if +// HTTP3 is enabled in the upstream options). If this attempt is successful, +// it returns an HTTP3 transport, otherwise it returns the H1/H2 transport. +func (p *dnsOverHTTPS) createTransport() (t http.RoundTripper, err error) { tlsConfig, dialContext, err := p.boot.get() if err != nil { return nil, fmt.Errorf("bootstrapping %s: %w", p.boot.URL, err) } + // First, we attempt to create an HTTP3 transport. If the probe QUIC + // connection is established successfully, we'll be using HTTP3 for this + // upstream. + transportH3, err := p.createTransportH3(tlsConfig, dialContext) + if err == nil { + log.Debug("using HTTP/3 for this upstream: QUIC was faster") + return transportH3, nil + } + + log.Debug("using HTTP/2 for this upstream: %v", err) + + if !p.supportsHTTP() { + return nil, errors.Error("HTTP1/1 and HTTP2 are not supported by this upstream") + } + transport := &http.Transport{ TLSClientConfig: tlsConfig, DisableCompression: true, @@ -210,3 +257,185 @@ func (p *dnsOverHTTPS) createTransport() (*http.Transport, error) { return transport, nil } + +// createTransportH3 tries to create an HTTP/3 transport for this upstream. +// We should be able to fall back to H1/H2 in case if HTTP/3 is unavailable or +// if it is too slow. In order to do that, this method will run two probes +// in parallel (one for TLS, the other one for QUIC) and if QUIC is faster it +// will create the *http3.RoundTripper instance. +func (p *dnsOverHTTPS) createTransportH3( + tlsConfig *tls.Config, + dialContext dialHandler, +) (roundTripper *http3.RoundTripper, err error) { + if !p.supportsH3() { + return nil, errors.Error("HTTP3 support is not enabled") + } + + addr, err := p.probeH3(tlsConfig, dialContext) + if err != nil { + return nil, err + } + + return &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) + }, + DisableCompression: true, + TLSClientConfig: tlsConfig, + QuicConfig: p.quicConfig, + }, nil +} + +// probeH3 runs a test to check whether QUIC is faster than TLS for this +// upstream. If the test is successful it will return the address that we +// should use to establish the QUIC connections. +func (p *dnsOverHTTPS) probeH3( + tlsConfig *tls.Config, + dialContext dialHandler, +) (addr string, err error) { + // We're using bootstrapped address instead of what's passed to the function + // it does not create an actual connection, but it helps us determine + // what IP is actually reachable (when there are v4/v6 addresses). + rawConn, err := dialContext(context.Background(), "udp", "") + if err != nil { + return "", fmt.Errorf("failed to dial: %w", err) + } + // It's never actually used. + _ = rawConn.Close() + + udpConn, ok := rawConn.(*net.UDPConn) + if !ok { + return "", fmt.Errorf("not a UDP connection to %s", p.Address()) + } + + addr = udpConn.RemoteAddr().String() + + // Avoid spending time on probing if this upstream only supports HTTP/3. + if p.supportsH3() && !p.supportsHTTP() { + return addr, nil + } + + // Use a new *tls.Config with empty session cache for probe connections. + // Surprisingly, this is really important since otherwise it invalidates + // the existing cache. + // TODO(ameshkov): figure out why the sessions cache invalidates here. + probeTLSCfg := tlsConfig.Clone() + probeTLSCfg.ClientSessionCache = nil + + // Do not expose probe connections to the callbacks that are passed to + // the bootstrap options to avoid side-effects. + // TODO(ameshkov): consider exposing, somehow mark that this is a probe. + probeTLSCfg.VerifyPeerCertificate = nil + probeTLSCfg.VerifyConnection = nil + + // Run probeQUIC and probeTLS in parallel and see which one is faster. + chQuic := make(chan error, 1) + chTLS := make(chan error, 1) + go p.probeQUIC(addr, probeTLSCfg, chQuic) + go p.probeTLS(dialContext, probeTLSCfg, chTLS) + + select { + case quicErr := <-chQuic: + if quicErr != nil { + // QUIC failed, return error since HTTP3 was not preferred. + return "", quicErr + } + + // Return immediately, QUIC was faster. + return addr, quicErr + case tlsErr := <-chTLS: + if tlsErr != nil { + // Return immediately, TLS failed. + log.Debug("probing TLS: %v", tlsErr) + return addr, nil + } + + return "", errors.Error("TLS was faster than QUIC, prefer it") + } +} + +// probeQUIC attempts to establish a QUIC connection to the specified address. +// We run probeQUIC and probeTLS in parallel and see which one is faster. +func (p *dnsOverHTTPS) probeQUIC(addr string, tlsConfig *tls.Config, ch chan error) { + startTime := time.Now() + + timeout := p.boot.options.Timeout + if timeout == 0 { + timeout = dialTimeout + } + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeout)) + defer cancel() + + conn, err := quic.DialAddrEarlyContext(ctx, addr, tlsConfig, p.quicConfig) + if err != nil { + ch <- fmt.Errorf("opening QUIC connection to %s: %w", p.Address(), err) + return + } + + // Ignore the error since there's no way we can use it for anything useful. + _ = conn.CloseWithError(QUICCodeNoError, "") + + ch <- nil + + elapsed := time.Now().Sub(startTime) + log.Debug("elapsed on establishing a QUIC connection: %s", elapsed) +} + +// probeTLS attempts to establish a TLS connection to the specified address. We +// run probeQUIC and probeTLS in parallel and see which one is faster. +func (p *dnsOverHTTPS) probeTLS(dialContext dialHandler, tlsConfig *tls.Config, ch chan error) { + startTime := time.Now() + + conn, err := tlsDial(dialContext, "tcp", tlsConfig) + if err != nil { + ch <- fmt.Errorf("opening TLS connection: %w", err) + return + } + + // Ignore the error since there's no way we can use it for anything useful. + _ = conn.Close() + + ch <- nil + + elapsed := time.Now().Sub(startTime) + log.Debug("elapsed on establishing a TLS connection: %s", elapsed) +} + +// supportsH3 returns true if HTTP/3 is supported by this upstream. +func (p *dnsOverHTTPS) supportsH3() (ok bool) { + for _, v := range p.supportedHTTPVersions() { + if v == HTTPVersion3 { + return true + } + } + + return false +} + +// supportsHTTP returns true if HTTP/1.1 or HTTP2 is supported by this upstream. +func (p *dnsOverHTTPS) supportsHTTP() (ok bool) { + for _, v := range p.supportedHTTPVersions() { + if v == HTTPVersion11 || v == HTTPVersion2 { + return true + } + } + + return false +} + +// supportedHTTPVersions returns the list of supported HTTP versions. +func (p *dnsOverHTTPS) supportedHTTPVersions() (v []HTTPVersion) { + v = p.boot.options.HTTPVersions + if v == nil { + v = DefaultHTTPVersions + } + + return v +} diff --git a/upstream/upstream_doh_test.go b/upstream/upstream_doh_test.go new file mode 100644 index 0000000000000000000000000000000000000000..16a9a09f44fd01cb325aa2055d47c56b0f170d6a --- /dev/null +++ b/upstream/upstream_doh_test.go @@ -0,0 +1,264 @@ +package upstream + +import ( + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "net" + "net/http" + "testing" + "time" + + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestUpstreamDoH(t *testing.T) { + testCases := []struct { + name string + http3Enabled bool + httpVersions []HTTPVersion + delayHandshakeH3 time.Duration + delayHandshakeH2 time.Duration + expectedProtocol HTTPVersion + }{{ + name: "http1.1_h2", + http3Enabled: false, + httpVersions: []HTTPVersion{HTTPVersion11, HTTPVersion2}, + expectedProtocol: HTTPVersion2, + }, { + name: "fallback_to_http2", + http3Enabled: false, + httpVersions: []HTTPVersion{HTTPVersion3, HTTPVersion2}, + expectedProtocol: HTTPVersion2, + }, { + name: "http3", + http3Enabled: true, + httpVersions: []HTTPVersion{HTTPVersion3}, + expectedProtocol: HTTPVersion3, + }, { + name: "race_http3_faster", + http3Enabled: true, + httpVersions: []HTTPVersion{HTTPVersion3, HTTPVersion2}, + delayHandshakeH2: time.Second, + expectedProtocol: HTTPVersion3, + }, { + name: "race_http2_faster", + http3Enabled: true, + httpVersions: []HTTPVersion{HTTPVersion3, HTTPVersion2}, + delayHandshakeH3: time.Second, + expectedProtocol: HTTPVersion2, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + srv := startDoHServer(t, testDoHServerOptions{ + http3Enabled: tc.http3Enabled, + delayHandshakeH2: tc.delayHandshakeH2, + delayHandshakeH3: tc.delayHandshakeH3, + }) + t.Cleanup(srv.Shutdown) + + // Create a DNS-over-HTTPS upstream. + address := fmt.Sprintf("https://%s/dns-query", srv.addr) + + var lastState tls.ConnectionState + u, err := AddressToUpstream( + address, + &Options{ + InsecureSkipVerify: true, + HTTPVersions: tc.httpVersions, + VerifyConnection: func(state tls.ConnectionState) (err error) { + if state.NegotiatedProtocol != string(tc.expectedProtocol) { + return fmt.Errorf( + "expected %s, got %s", + tc.expectedProtocol, + state.NegotiatedProtocol, + ) + } + lastState = state + return nil + }, + }, + ) + require.NoError(t, err) + + // Test that it responds properly. + for i := 0; i < 10; i++ { + checkUpstream(t, u, address) + } + + doh := u.(*dnsOverHTTPS) + + // Trigger re-connection. + doh.client = nil + + // Force it to establish the connection again. + checkUpstream(t, u, address) + + // Check that TLS session was resumed properly. + require.True(t, lastState.DidResume) + }) + } +} + +// testDoHServerOptions allows customizing testDoHServer behavior. +type testDoHServerOptions struct { + http3Enabled bool + delayHandshakeH2 time.Duration + delayHandshakeH3 time.Duration +} + +// testDoHServer is an instance of a test DNS-over-HTTPS server that we use +// for tests. +type testDoHServer 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 + + // server is an HTTP/1.1 and HTTP/2 server. + server *http.Server + + // serverH3 is an HTTP/3 server. + serverH3 *http3.Server +} + +// Shutdown stops the DOH server. +func (s *testDoHServer) Shutdown() { + if s.server != nil { + _ = s.server.Shutdown(context.Background()) + } + + if s.serverH3 != nil { + _ = s.serverH3.Close() + } +} + +// startDoHServer starts a new DNS-over-HTTPS server on a random port and +// returns the instance of this server. Depending on whether http3Enabled is +// set to true or false it will or will not initialize a HTTP/3 server. +func startDoHServer( + t *testing.T, + opts testDoHServerOptions, +) (s *testDoHServer) { + tlsConfig := createServerTLSConfig(t, "127.0.0.1") + handler := createDoHHandler() + + // Step one is to create a regular HTTP server, we'll always have it + // running. + server := &http.Server{ + Handler: handler, + } + + // Listen TCP first. + tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") + require.NoError(t, err) + + tcpListen, err := net.ListenTCP("tcp", tcpAddr) + require.NoError(t, err) + + tlsConfigH2 := tlsConfig.Clone() + tlsConfigH2.NextProtos = []string{string(HTTPVersion2), string(HTTPVersion11)} + tlsConfigH2.GetConfigForClient = func(_ *tls.ClientHelloInfo) (*tls.Config, error) { + if opts.delayHandshakeH2 > 0 { + time.Sleep(opts.delayHandshakeH2) + } + return nil, nil + } + tlsListen := tls.NewListener(tcpListen, tlsConfigH2) + + // Run the H1/H2 server. + go server.Serve(tlsListen) + + // Get the real address that the listener now listens to. + tcpAddr = tcpListen.Addr().(*net.TCPAddr) + + var serverH3 *http3.Server + + if opts.http3Enabled { + tlsConfigH3 := tlsConfig.Clone() + tlsConfigH3.NextProtos = []string{string(HTTPVersion3)} + tlsConfigH3.GetConfigForClient = func(_ *tls.ClientHelloInfo) (*tls.Config, error) { + if opts.delayHandshakeH3 > 0 { + time.Sleep(opts.delayHandshakeH3) + } + return nil, nil + } + + serverH3 = &http3.Server{ + TLSConfig: tlsConfig.Clone(), + QuicConfig: &quic.Config{}, + Handler: handler, + } + + // Listen UDP for the H3 server. Reuse the same port as was used for the + // TCP listener. + udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("127.0.0.1:%d", tcpAddr.Port)) + require.NoError(t, err) + + udpListen, err := net.ListenUDP("udp", udpAddr) + require.NoError(t, err) + + // Run the H3 server. + go serverH3.Serve(udpListen) + } + + return &testDoHServer{ + tlsConfig: tlsConfig, + server: server, + serverH3: serverH3, + // Save the address that the server listens to. + addr: tcpAddr.String(), + } +} + +// createDoHHandler returns a very simple http.Handler that reads the incoming +// request and returns with a test message. +func createDoHHandler() (h http.Handler) { + mux := http.NewServeMux() + mux.HandleFunc("/dns-query", func(w http.ResponseWriter, r *http.Request) { + dnsParam := r.URL.Query().Get("dns") + buf, err := base64.RawURLEncoding.DecodeString(dnsParam) + if err != nil { + http.Error( + w, + fmt.Sprintf("internal error: %s", err), + http.StatusInternalServerError, + ) + return + } + + m := &dns.Msg{} + err = m.Unpack(buf) + if err != nil { + http.Error( + w, + fmt.Sprintf("internal error: %s", err), + http.StatusInternalServerError, + ) + return + } + + resp := respondToTestMessage(m) + + buf, err = resp.Pack() + if err != nil { + http.Error( + w, + fmt.Sprintf("internal error: %s", err), + http.StatusInternalServerError, + ) + return + } + + w.Header().Set("Content-Type", "application/dns-message") + _, err = w.Write(buf) + }) + + return mux +} diff --git a/upstream/upstream_dot.go b/upstream/upstream_dot.go index 4b31d33096e3e910dfe93846ce411f05f98f2bf5..380478d34eaed09551c3fb9b0950d6291d0396d7 100644 --- a/upstream/upstream_dot.go +++ b/upstream/upstream_dot.go @@ -11,9 +11,8 @@ import ( "github.com/miekg/dns" ) -// -// DNS-over-TLS -// +// dnsOverTLS is a struct that implements the Upstream interface for the +// DNS-over-TLS protocol. type dnsOverTLS struct { boot *bootstrapper pool *TLSPool @@ -37,8 +36,10 @@ func newDoT(uu *url.URL, opts *Options) (u Upstream, err error) { return &dnsOverTLS{boot: b}, nil } +// Address implements the Upstream interface for *dnsOverTLS. func (p *dnsOverTLS) Address() string { return p.boot.URL.String() } +// Exchange implements the Upstream interface for *dnsOverTLS. func (p *dnsOverTLS) Exchange(m *dns.Msg) (reply *dns.Msg, err error) { var pool *TLSPool p.RLock() diff --git a/upstream/upstream_plain.go b/upstream/upstream_plain.go index 1b6d17813736c94b467a5320a8742f8c50b438e8..f5f4efd67619d626e21e668e8e41a262097af3a2 100644 --- a/upstream/upstream_plain.go +++ b/upstream/upstream_plain.go @@ -8,9 +8,8 @@ import ( "github.com/miekg/dns" ) -// -// plain DNS -// +// plainDNS is a struct that implements the Upstream interface for the regular +// DNS protocol. type plainDNS struct { address string timeout time.Duration @@ -31,7 +30,7 @@ func newPlain(uu *url.URL, timeout time.Duration, preferTCP bool) (u *plainDNS) } } -// Address returns the original address that we've put in initially, not resolved one +// Address implements the Upstream interface for *plainDNS. func (p *plainDNS) Address() string { if p.preferTCP { return "tcp://" + p.address @@ -40,6 +39,7 @@ func (p *plainDNS) Address() string { return p.address } +// Exchange implements the Upstream interface for *plainDNS. func (p *plainDNS) Exchange(m *dns.Msg) (*dns.Msg, error) { if p.preferTCP { tcpClient := dns.Client{Net: "tcp", Timeout: p.timeout} diff --git a/upstream/upstream_pool.go b/upstream/upstream_pool.go index 276fa04fa0240900725c8a737774f742ba35ae5d..9c3ddd3bc8ecb5c8ca4f81e15cd5b47365f6a91b 100644 --- a/upstream/upstream_pool.go +++ b/upstream/upstream_pool.go @@ -11,35 +11,40 @@ import ( "github.com/AdguardTeam/golibs/log" ) +// dialTimeout is the global timeout for establishing a TLS connection. +// TODO(ameshkov): use bootstrap timeout instead. const dialTimeout = 10 * time.Second // TLSPool is a connections pool for the DNS-over-TLS Upstream. // // Example: -// pool := TLSPool{Address: "tls://1.1.1.1:853"} -// netConn, err := pool.Get() -// if err != nil {panic(err)} -// c := dns.Conn{Conn: netConn} -// q := dns.Msg{} -// q.SetQuestion("google.com.", dns.TypeA) -// log.Println(q) -// err = c.WriteMsg(&q) -// if err != nil {panic(err)} -// r, err := c.ReadMsg() -// if err != nil {panic(err)} -// log.Println(r) -// pool.Put(c.Conn) +// +// pool := TLSPool{Address: "tls://1.1.1.1:853"} +// netConn, err := pool.Get() +// if err != nil {panic(err)} +// c := dns.Conn{Conn: netConn} +// q := dns.Msg{} +// q.SetQuestion("google.com.", dns.TypeA) +// log.Println(q) +// err = c.WriteMsg(&q) +// if err != nil {panic(err)} +// r, err := c.ReadMsg() +// if err != nil {panic(err)} +// log.Println(r) +// pool.Put(c.Conn) type TLSPool struct { boot *bootstrapper - // connections - conns []net.Conn - connsMutex sync.Mutex // protects conns + // conns is the list of connections available in the pool. + conns []net.Conn + // connsMutex protects conns. + connsMutex sync.Mutex } -// Get gets or creates a new TLS connection +// Get gets a connection from the pool (if there's one available) or creates +// a new TLS connection. func (n *TLSPool) Get() (net.Conn, error) { - // get the connection from the slice inside the lock + // Get the connection from the slice inside the lock. var c net.Conn n.connsMutex.Lock() num := len(n.conns) @@ -50,7 +55,7 @@ func (n *TLSPool) Get() (net.Conn, error) { } n.connsMutex.Unlock() - // if we got connection from the slice, update deadline and return it. + // If we got connection from the slice, update deadline and return it. if c != nil { err := c.SetDeadline(time.Now().Add(dialTimeout)) @@ -64,7 +69,7 @@ func (n *TLSPool) Get() (net.Conn, error) { return n.Create() } -// Create creates a new connection for the pool (but not puts it there) +// Create creates a new connection for the pool (but not puts it there). func (n *TLSPool) Create() (net.Conn, error) { tlsConfig, dialContext, err := n.boot.get() if err != nil { @@ -80,7 +85,7 @@ func (n *TLSPool) Create() (net.Conn, error) { return conn, nil } -// Put returns connection to the pool +// Put returns the connection to the pool. func (n *TLSPool) Put(c net.Conn) { if c == nil { return @@ -90,16 +95,18 @@ func (n *TLSPool) Put(c net.Conn) { n.connsMutex.Unlock() } -// tlsDial is basically the same as tls.DialWithDialer, but we will call our own dialContext function to get connection +// tlsDial is basically the same as tls.DialWithDialer, but we will call our own +// dialContext function to get connection. func tlsDial(dialContext dialHandler, network string, config *tls.Config) (*tls.Conn, error) { - // we're using bootstrapped address instead of what's passed to the function + // We're using bootstrapped address instead of what's passed + // to the function. rawConn, err := dialContext(context.Background(), network, "") if err != nil { return nil, err } - // we want the timeout to cover the whole process: TCP connection and TLS handshake - // dialTimeout will be used as connection deadLine + // We want the timeout to cover the whole process: TCP connection and + // TLS handshake dialTimeout will be used as connection deadLine. conn := tls.Client(rawConn, config) err = conn.SetDeadline(time.Now().Add(dialTimeout)) if err != nil { diff --git a/upstream/upstream_pool_test.go b/upstream/upstream_pool_test.go index fe59edfa02d6dd7e9030b716fe0cd4f6a4fb7b8d..85b73ad3bb0e7651960b2c6dae5b38fd042d84a8 100644 --- a/upstream/upstream_pool_test.go +++ b/upstream/upstream_pool_test.go @@ -1,48 +1,51 @@ package upstream import ( + "crypto/tls" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestTLSPoolReconnect(t *testing.T) { + var lastState tls.ConnectionState u, err := AddressToUpstream( "tls://one.one.one.one", &Options{ Bootstrap: []string{"8.8.8.8:53"}, Timeout: timeout, + VerifyConnection: func(state tls.ConnectionState) error { + lastState = state + return nil + }, }, ) - if err != nil { - t.Fatalf("cannot create upstream: %s", err) - } + require.NoError(t, err) - // Send the first test message + // Send the first test message. req := createTestMessage() reply, err := u.Exchange(req) - if err != nil { - t.Fatalf("first DNS message failed: %s", err) - } + require.NoError(t, err) requireResponse(t, req, reply) - // Now let's close the pooled connection and return it back to the pool + // Now let's close the pooled connection and return it back to the pool. p := u.(*dnsOverTLS) conn, _ := p.pool.Get() conn.Close() p.pool.Put(conn) - // Send the second test message + // Send the second test message. req = createTestMessage() reply, err = u.Exchange(req) - if err != nil { - t.Fatalf("second DNS message failed: %s", err) - } + require.NoError(t, err) requireResponse(t, req, reply) // Now assert that the number of connections in the pool is not changed - if len(p.pool.conns) != 1 { - t.Fatal("wrong number of pooled connections") - } + require.Len(t, p.pool.conns, 1) + + // Check that the session was resumed on the last attempt. + require.True(t, lastState.DidResume) } func TestTLSPoolDeadLine(t *testing.T) { diff --git a/upstream/upstream_quic.go b/upstream/upstream_quic.go index e4ca6df3d6c6b9656ce5eded37ea12433c468ca7..eb98fd171726a771cf9ef678ed43de8d696f09c6 100644 --- a/upstream/upstream_quic.go +++ b/upstream/upstream_quic.go @@ -15,30 +15,37 @@ import ( ) const ( - // DoQCodeNoError is used when the connection or stream needs to be closed, + // QUICCodeNoError is used when the connection or stream needs to be closed, // but there is no error to signal. - DoQCodeNoError = quic.ApplicationErrorCode(0) - // DoQCodeInternalError signals that the DoQ implementation encountered + QUICCodeNoError = quic.ApplicationErrorCode(0) + // QUICCodeInternalError signals that the DoQ implementation encountered // an internal error and is incapable of pursuing the transaction or the // connection. - DoQCodeInternalError = quic.ApplicationErrorCode(1) - // DoQCodeProtocolError signals that the DoQ implementation encountered + QUICCodeInternalError = quic.ApplicationErrorCode(1) + // QUICCodeProtocolError signals that the DoQ implementation encountered // a protocol error and is forcibly aborting the connection. - DoQCodeProtocolError = quic.ApplicationErrorCode(2) + 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 + // KeepAlive field set to true This value is specified in + // https://pkg.go.dev/github.com/lucas-clemente/quic-go/internal/protocol#MaxKeepAliveInterval. + // + // TODO(ameshkov): Consider making it configurable. + QUICKeepAlivePeriod = time.Second * 20 ) -// -// dnsOverQUIC is a DNS-over-QUIC implementation according to the spec: -// https://www.rfc-editor.org/rfc/rfc9250.html -// +// dnsOverQUIC is a struct that implements the Upstream interface for the +// DNS-over-QUIC protocol (spec: https://www.rfc-editor.org/rfc/rfc9250.html). type dnsOverQUIC struct { // boot is a bootstrap DNS abstraction that is used to resolve the upstream // server's address and open a network connection to it. boot *bootstrapper - // tokenStore is a QUIC token store that is used across QUIC connections. - // Since the QUIC config is re-created when a connection is (re-)opened - // the tokenStore is instead saved as part of the dnsOverQUIC struct. - tokenStore quic.TokenStore + // quicConfig is the QUIC configuration that is used for establishing + // connections to the upstream. This configuration includes the TokenStore + // that needs to be stored for the lifetime of dnsOverQUIC since we can + // re-create the connection. + quicConfig *quic.Config // conn is the current active QUIC connection. It can be closed and // re-opened when needed. conn quic.Connection @@ -62,11 +69,24 @@ func newDoQ(uu *url.URL, opts *Options) (u Upstream, err error) { return nil, fmt.Errorf("creating quic bootstrapper: %w", err) } - return &dnsOverQUIC{boot: b, tokenStore: quic.NewLRUTokenStore(1, 10)}, nil + return &dnsOverQUIC{ + 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), + }, + }, nil } +// Address implements the Upstream interface for *dnsOverQUIC. 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) @@ -95,13 +115,13 @@ 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(DoQCodeInternalError) + p.closeConnWithError(QUICCodeInternalError) return nil, fmt.Errorf("open new stream to %s: %w", p.Address(), err) } _, err = stream.Write(proxyutil.AddPrefix(buf)) if err != nil { - p.closeConnWithError(DoQCodeInternalError) + p.closeConnWithError(QUICCodeInternalError) return nil, fmt.Errorf("failed to write to a QUIC stream: %w", err) } @@ -117,7 +137,7 @@ func (p *dnsOverQUIC) Exchange(m *dns.Msg) (res *dns.Msg, err error) { // 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(DoQCodeProtocolError) + p.closeConnWithError(QUICCodeProtocolError) } return res, err } @@ -152,7 +172,7 @@ func (p *dnsOverQUIC) getConnection(useCached bool) (quic.Connection, error) { } if conn != nil { // we're recreating the connection, let's create a new one. - _ = conn.CloseWithError(DoQCodeNoError, "") + _ = conn.CloseWithError(QUICCodeNoError, "") } p.RUnlock() @@ -162,11 +182,10 @@ func (p *dnsOverQUIC) getConnection(useCached bool) (quic.Connection, error) { 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 conn. + // 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 @@ -224,17 +243,8 @@ func (p *dnsOverQUIC) openConnection() (conn quic.Connection, err error) { } addr := udpConn.RemoteAddr().String() - quicConfig := &quic.Config{ - // Set the keep alive interval to 20s as it would be in the - // quic-go@v0.27.1 with KeepAlive field set to true. This value is - // specified in - // https://pkg.go.dev/github.com/lucas-clemente/quic-go/internal/protocol#MaxKeepAliveInterval. - // - // TODO(ameshkov): Consider making it configurable. - KeepAlivePeriod: 20 * time.Second, - TokenStore: p.tokenStore, - } - conn, err = quic.DialAddrEarlyContext(context.Background(), addr, tlsConfig, quicConfig) + + conn, err = quic.DialAddrEarlyContext(context.Background(), addr, tlsConfig, p.quicConfig) if err != nil { return nil, fmt.Errorf("opening quic connection to %s: %w", p.Address(), err) } diff --git a/upstream/upstream_quic_test.go b/upstream/upstream_quic_test.go index f5d2ac5b73c6783e79c3c2912fa042d2d0ae8d62..0a2fe21f048d1f7809c60a80d4655526c63383c5 100644 --- a/upstream/upstream_quic_test.go +++ b/upstream/upstream_quic_test.go @@ -1,6 +1,7 @@ package upstream import ( + "crypto/tls" "testing" "github.com/lucas-clemente/quic-go" @@ -10,7 +11,16 @@ import ( func TestUpstreamDoQ(t *testing.T) { // Create a DNS-over-QUIC upstream address := "quic://dns.adguard.com" - u, err := AddressToUpstream(address, &Options{InsecureSkipVerify: true}) + var lastState tls.ConnectionState + u, err := AddressToUpstream( + address, + &Options{ + VerifyConnection: func(state tls.ConnectionState) error { + lastState = state + return nil + }, + }, + ) require.NoError(t, err) uq := u.(*dnsOverQUIC) @@ -23,8 +33,17 @@ func TestUpstreamDoQ(t *testing.T) { if conn == nil { conn = uq.conn } else { - // This way we test that the conn is properly reused + // This way we test that the conn is properly reused. require.Equal(t, conn, uq.conn) } } + + // Close the connection (make sure that we re-establish the connection). + _ = conn.CloseWithError(quic.ApplicationErrorCode(0), "") + + // Try to establish it again. + checkUpstream(t, u, address) + + // Make sure that the session has been resumed. + require.True(t, lastState.DidResume) } diff --git a/upstream/upstream_test.go b/upstream/upstream_test.go index 4a0901c4143156260d16c277f4eb56ccc728daf2..dcf9e19a859f7d66b9afd2f9c3619681674148c6 100644 --- a/upstream/upstream_test.go +++ b/upstream/upstream_test.go @@ -1,8 +1,16 @@ package upstream import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" "io" + "math/big" "net" "net/url" "os" @@ -192,6 +200,10 @@ func TestUpstreams(t *testing.T) { // Cloudflare DNS address: "quic://dns-unfiltered.adguard.com:784", bootstrap: []string{}, + }, { + // Google DNS (HTTP3) + address: "h3://dns.google/dns-query", + bootstrap: []string{}, }} for _, test := range upstreams { t.Run(test.address, func(t *testing.T) { @@ -237,6 +249,10 @@ func TestAddressToUpstream(t *testing.T) { addr: "https://one.one.one.one", opt: opt, want: "https://one.one.one.one:443", + }, { + addr: "h3://one.one.one.one", + opt: opt, + want: "https://one.one.one.one:443", }} for _, tc := range testCases { @@ -405,45 +421,6 @@ func TestUpstreamsWithServerIP(t *testing.T) { } } -func checkUpstream(t *testing.T, u Upstream, addr string) { - t.Helper() - - req := createTestMessage() - reply, err := u.Exchange(req) - require.NoErrorf(t, err, "couldn't talk to upstream %s", addr) - - requireResponse(t, req, reply) -} - -func createTestMessage() *dns.Msg { - return createHostTestMessage("google-public-dns-a.google.com") -} - -func createHostTestMessage(host string) (req *dns.Msg) { - return &dns.Msg{ - MsgHdr: dns.MsgHdr{ - Id: dns.Id(), - RecursionDesired: true, - }, - Question: []dns.Question{{ - Name: dns.Fqdn(host), - Qtype: dns.TypeA, - Qclass: dns.ClassINET, - }}, - } -} - -func requireResponse(t *testing.T, req, reply *dns.Msg) { - require.NotNil(t, reply) - require.Lenf(t, reply.Answer, 1, "wrong number of answers: %d", len(reply.Answer)) - require.Equal(t, req.Id, reply.Id) - - a, ok := reply.Answer[0].(*dns.A) - require.Truef(t, ok, "wrong answer type: %v", reply.Answer[0]) - - require.Equalf(t, net.IPv4(8, 8, 8, 8), a.A.To16(), "wrong answer: %v", a.A) -} - func TestAddPort(t *testing.T) { testCases := []struct { name string @@ -498,3 +475,131 @@ func TestAddPort(t *testing.T) { }) } } + +func checkUpstream(t *testing.T, u Upstream, addr string) { + t.Helper() + + req := createTestMessage() + reply, err := u.Exchange(req) + require.NoErrorf(t, err, "couldn't talk to upstream %s", addr) + + requireResponse(t, req, reply) +} + +func createTestMessage() (m *dns.Msg) { + return createHostTestMessage("google-public-dns-a.google.com") +} + +func respondToTestMessage(m *dns.Msg) (resp *dns.Msg) { + resp = &dns.Msg{} + resp.SetReply(m) + resp.Answer = append(resp.Answer, &dns.A{ + A: net.IPv4(8, 8, 8, 8), + Hdr: dns.RR_Header{ + Name: "google-public-dns-a.google.com.", + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 100, + }, + }) + + return resp +} + +func createHostTestMessage(host string) (req *dns.Msg) { + return &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{ + Name: dns.Fqdn(host), + Qtype: dns.TypeA, + Qclass: dns.ClassINET, + }}, + } +} + +func requireResponse(t *testing.T, req, reply *dns.Msg) { + require.NotNil(t, reply) + require.Lenf(t, reply.Answer, 1, "wrong number of answers: %d", len(reply.Answer)) + require.Equal(t, req.Id, reply.Id) + + a, ok := reply.Answer[0].(*dns.A) + require.Truef(t, ok, "wrong answer type: %v", reply.Answer[0]) + + require.Equalf(t, net.IPv4(8, 8, 8, 8), a.A.To16(), "wrong answer: %v", a.A) +} + +// createServerTLSConfig creates a test server TLS configuration. It returns +// a *tls.Config that can be used for both the server and the client. +func createServerTLSConfig(t *testing.T, tlsServerName string) (tlsConfig *tls.Config) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err) + + notBefore := time.Now() + notAfter := notBefore.Add(5 * 365 * time.Hour * 24) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"AdGuard Tests"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + template.DNSNames = append(template.DNSNames, tlsServerName) + + derBytes, err := x509.CreateCertificate( + rand.Reader, + &template, + &template, + publicKey(privateKey), + privateKey, + ) + require.NoError(t, err) + + certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + keyPem := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }, + ) + + cert, err := tls.X509KeyPair(certPem, keyPem) + require.NoError(t, err) + + roots := x509.NewCertPool() + roots.AppendCertsFromPEM(certPem) + + tlsConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + ServerName: tlsServerName, + RootCAs: roots, + MinVersion: tls.VersionTLS12, + } + + return tlsConfig +} + +// publicKey extracts the public key from the specified private key. +func publicKey(priv any) (pub any) { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + default: + return nil + } +} diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/body.go b/vendor/github.com/lucas-clemente/quic-go/http3/body.go new file mode 100644 index 0000000000000000000000000000000000000000..d6e704ebcb169646fb06c040c6e7b4c1a01ce3dd Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/body.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/client.go b/vendor/github.com/lucas-clemente/quic-go/http3/client.go new file mode 100644 index 0000000000000000000000000000000000000000..90115e4b1b82926b8ebfc9edcc466c733c3aae33 Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/client.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/error_codes.go b/vendor/github.com/lucas-clemente/quic-go/http3/error_codes.go new file mode 100644 index 0000000000000000000000000000000000000000..d87eef4ae076a40de382cee10cb5833b0e2a9d8f Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/error_codes.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/frames.go b/vendor/github.com/lucas-clemente/quic-go/http3/frames.go new file mode 100644 index 0000000000000000000000000000000000000000..af9f28eb9f5b1a02e2e01b5db3231d9ee2ae6950 Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/frames.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/gzip_reader.go b/vendor/github.com/lucas-clemente/quic-go/http3/gzip_reader.go new file mode 100644 index 0000000000000000000000000000000000000000..01983ac77b8c79eb430d13f89ea40bba3b29a89c Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/gzip_reader.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/http_stream.go b/vendor/github.com/lucas-clemente/quic-go/http3/http_stream.go new file mode 100644 index 0000000000000000000000000000000000000000..4c69068cdbb5c815822fdcc531f5225b5e26026c Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/http_stream.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/request.go b/vendor/github.com/lucas-clemente/quic-go/http3/request.go new file mode 100644 index 0000000000000000000000000000000000000000..0b9a7278cc0a4442057190304a5fec6f3456465a Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/request.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/request_writer.go b/vendor/github.com/lucas-clemente/quic-go/http3/request_writer.go new file mode 100644 index 0000000000000000000000000000000000000000..3dd1cbaef341fb716b2d3e38b998ed82fe479869 Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/request_writer.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/response_writer.go b/vendor/github.com/lucas-clemente/quic-go/http3/response_writer.go new file mode 100644 index 0000000000000000000000000000000000000000..70a7cd3f484382167a8b7acead1d2cbe237099b3 Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/response_writer.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/roundtrip.go b/vendor/github.com/lucas-clemente/quic-go/http3/roundtrip.go new file mode 100644 index 0000000000000000000000000000000000000000..5cde95a62fd8a6ffeee9bc34fbf248f09d1c7b15 Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/roundtrip.go differ diff --git a/vendor/github.com/lucas-clemente/quic-go/http3/server.go b/vendor/github.com/lucas-clemente/quic-go/http3/server.go new file mode 100644 index 0000000000000000000000000000000000000000..086bc8e256452aa23187438c9785ef36bd5b33cc Binary files /dev/null and b/vendor/github.com/lucas-clemente/quic-go/http3/server.go differ diff --git a/vendor/github.com/marten-seemann/qpack/.codecov.yml b/vendor/github.com/marten-seemann/qpack/.codecov.yml new file mode 100644 index 0000000000000000000000000000000000000000..00064af33101737796f3746de1a6b8059bab0b3f Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/.codecov.yml differ diff --git a/vendor/github.com/marten-seemann/qpack/.gitignore b/vendor/github.com/marten-seemann/qpack/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..66c189a0904978caf9f02955729eb557c2856656 Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/.gitignore differ diff --git a/vendor/github.com/marten-seemann/qpack/.gitmodules b/vendor/github.com/marten-seemann/qpack/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..5ac16f084afde78701f1ae3b0c1c963ff2150fca Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/.gitmodules differ diff --git a/vendor/github.com/marten-seemann/qpack/.golangci.yml b/vendor/github.com/marten-seemann/qpack/.golangci.yml new file mode 100644 index 0000000000000000000000000000000000000000..4a91adc77ace30b4359b17c42ff01ef5bd2d2e20 Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/.golangci.yml differ diff --git a/vendor/github.com/marten-seemann/qpack/LICENSE.md b/vendor/github.com/marten-seemann/qpack/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..1ac5a2d9ae6a99a7da8cf29193770a0d8c04e430 Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/LICENSE.md differ diff --git a/vendor/github.com/marten-seemann/qpack/README.md b/vendor/github.com/marten-seemann/qpack/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a621825ac9d5b5d7f5162d7a62c739a63f1a1b56 Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/README.md differ diff --git a/vendor/github.com/marten-seemann/qpack/decoder.go b/vendor/github.com/marten-seemann/qpack/decoder.go new file mode 100644 index 0000000000000000000000000000000000000000..c9001941333ebe646960641122a87fb408179fb0 Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/decoder.go differ diff --git a/vendor/github.com/marten-seemann/qpack/encoder.go b/vendor/github.com/marten-seemann/qpack/encoder.go new file mode 100644 index 0000000000000000000000000000000000000000..13e0ad2c7d1f9631e8fef48c655b4da24acc4a08 Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/encoder.go differ diff --git a/vendor/github.com/marten-seemann/qpack/header_field.go b/vendor/github.com/marten-seemann/qpack/header_field.go new file mode 100644 index 0000000000000000000000000000000000000000..4c043a9928c5a45d38de71bdb83acd9f7687ec9d Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/header_field.go differ diff --git a/vendor/github.com/marten-seemann/qpack/static_table.go b/vendor/github.com/marten-seemann/qpack/static_table.go new file mode 100644 index 0000000000000000000000000000000000000000..930e83c7c12c392c5d3c882b2a0231bcd021bfa6 Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/static_table.go differ diff --git a/vendor/github.com/marten-seemann/qpack/varint.go b/vendor/github.com/marten-seemann/qpack/varint.go new file mode 100644 index 0000000000000000000000000000000000000000..28d71122e139c922be0232e48d26da18432366ee Binary files /dev/null and b/vendor/github.com/marten-seemann/qpack/varint.go differ diff --git a/vendor/modules.txt b/vendor/modules.txt index cf5e87e03dec0cb446390ef3e2043fa1f337cbe7..170d03040e5282e5a3f9ca4ddfd1e9471e212b05 100644 Binary files a/vendor/modules.txt and b/vendor/modules.txt differ