Skip to content
Commits on Source (8)
  • justus-forks's avatar
    Update upstream_quic.go to add QUIC token store · 724e6904
    justus-forks authored
    Add QUIC token store to dnsOverQUIC struct so that it can be (re-)used across QUIC connections. This enables avoiding the Retry mechanism and using 0-RTT in case the upstream server supports it.
    724e6904
  • justus-forks's avatar
    Update bootstrap.go to add a TLS session cache · 22646968
    justus-forks authored
    Add a TLS 1.3 client session cache to the TLS configuration for QUIC upstreams. This enables 0-RTT.
    22646968
  • justus-forks's avatar
    Update upstream_quic.go to dial with 0-RTT · ed01592f
    justus-forks authored
    Change quic.DialAddrContext to quic.DialAddrEarlyContext
    ed01592f
  • justus-forks's avatar
    Change token store origin capacity to 1 · e5a97efd
    justus-forks authored
    A token store is associated with each upstream, which should mean one origin only.
    e5a97efd
  • justus-forks's avatar
    Change session cache capacity to 10 · 32615418
    justus-forks authored
    32615418
  • justus-forks's avatar
    1dd831b0
  • Andrey Meshkov's avatar
    Pull request: Added DoH3 support, added TLS resumption · 823fa92f
    Andrey Meshkov authored
    Merge in DNS/dnsproxy from doh3 to master
    
    Squashed commit of the following:
    
    commit 93dc50875caf2df86ce08f22f5fb74e33b7b5ac0
    Author: Andrey Meshkov <am@adguard.com>
    Date:   Fri Sep 16 18:33:38 2022 +0300
    
        fix review comments
    
    commit d19fd61eb69f31c94a9374396cbbefeb566a2163
    Author: Andrey Meshkov <am@adguard.com>
    Date:   Fri Sep 16 17:28:21 2022 +0300
    
        upstream: added comments, minor fixes
    
    commit 9e4bf71275e9d1d3bc1cd72e27812548e8158402
    Author: Andrey Meshkov <am@adguard.com>
    Date:   Fri Sep 16 16:59:57 2022 +0300
    
        upstream: added DoH3 support, added TLS resumption
        The changes are pretty considerable in this PR.
    
        First of all, DoH3 support has been added to dnsOverHTTPS. I haven't added
        a new type of upstream for that, but added it to the already existing one.
        Configuring supported HTTP versions is possible via upstream.Options. When all
        versions are enabled, it will "probe" both TLS and QUIC and choose the one that
        was faster (just like it's done in Chrome).
    
        Command-line interface now supports a new argument "http3" that is supposed to
        enable HTTP/3 globally. At this point it will only enable it for upstreams, but
        in the future it will also enable it for the DoH server.
    
        One more important change here is the introduction of TLS sessions cache. It
        appears that we weren't benefiting from TLS session resumption mechanism at all,
        thank god this is finally fixed.
    
        Finally, AddressToUpstream now supports "h3://" scheme for those who want to try
        DoH3 for a particular upstream without enabling it globally. The reasoning for
        implementing this custom scheme is the following: currently, only a small share
        of public resolvers fully support DoH3. Users may not want to spend time
        "probing" every upstream for H3.
    
    commit 8c76e435860699a2d5815fc702b7a7e928eba3ed
    Merge: 1145771 1dd831b0
    Author: Andrey Meshkov <am@adguard.com>
    Date:   Fri Sep 16 12:05:34 2022 +0300
    
        Merge branch 'justus-forks-doq-0rtt' into doh3
    
    commit 1145771f7621be5778cf14b47ccfb4aa20d07c81
    Author: Andrey Meshkov <am@adguard.com>
    Date:   Fri Sep 16 11:45:42 2022 +0300
    
        upstream: initial attempt to add a DOH3 upstream
    823fa92f
  • Andrey Meshkov's avatar
    Pull request: proxy: added HTTP/3 support to the DNS-over-HTTPS server implementation · a03a56c8
    Andrey Meshkov authored
    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.
    a03a56c8
......@@ -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
......
......@@ -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
......@@ -171,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
......
......@@ -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"
)
......
......@@ -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)
}
......
......@@ -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
......@@ -24,6 +25,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
......
......@@ -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=
......@@ -42,6 +44,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 +59,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 +92,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 +110,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 +124,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=
......
......@@ -80,6 +80,10 @@ 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.
// 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
// --
......@@ -269,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.
......@@ -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,
......
......@@ -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
......
......@@ -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) {
......
......@@ -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
......
......@@ -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{}
......
......@@ -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 {
......
......@@ -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)
......
package proxy
import (
"crypto/tls"
"encoding/base64"
"fmt"
"io"
......@@ -11,57 +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)
return tcpListen.Addr().(*net.TCPAddr), nil
}
p.httpsServer = append(p.httpsServer, srv)
// 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) {
// - 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 (h *proxyHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Tracef("Incoming HTTPS request on %s", r.URL)
var buf []byte
......@@ -104,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
......@@ -117,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)
}
......@@ -159,11 +210,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.
......@@ -189,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
......@@ -208,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
}
......
......@@ -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,
}
}
......@@ -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
}
......@@ -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, "")
......
......@@ -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)
......
......@@ -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
......
......@@ -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,23 +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,
VerifyConnection: n.options.VerifyConnection,
}
// Depending on the URL scheme, we choose what ALPN will be advertised by
......@@ -209,7 +244,17 @@ 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
}
......@@ -217,7 +262,8 @@ func (n *bootstrapper) createTLSConfig(host string) *tls.Config {
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,
......@@ -238,16 +284,38 @@ 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...)
}
}
// 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
}