Skip to content

Commit

Permalink
Pull request: proxy: added HTTP/3 support to the DNS-over-HTTPS serve…
Browse files Browse the repository at this point in the history
…r implementation

Merge in DNS/dnsproxy from doh3server to master

Squashed commit of the following:

commit dd7f6ec
Author: Andrey Meshkov <am@adguard.com>
Date:   Tue Sep 20 14:17:51 2022 +0300

    upstream: fix review comments

commit 3b887f6
Author: Andrey Meshkov <am@adguard.com>
Date:   Tue Sep 20 00:14:19 2022 +0300

    proxy: added address validation logic

commit b29dc3c
Author: Andrey Meshkov <am@adguard.com>
Date:   Mon Sep 19 23:31:21 2022 +0300

    proxy: fix review comments, general improvements

commit 79f47f5
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 59cf92b
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 804dded
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.
  • Loading branch information
ameshkov committed Sep 20, 2022
1 parent 823fa92 commit a03a56c
Show file tree
Hide file tree
Showing 32 changed files with 3,140 additions and 294 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ Runs a DNS-over-HTTPS proxy on `127.0.0.1:443`.
./dnsproxy -l 127.0.0.1 --https-port=443 --tls-crt=example.crt --tls-key=example.key -u 8.8.8.8:53 -p 0
```
Runs a DNS-over-HTTPS proxy on `127.0.0.1:443` with HTTP/3 support.
```shell
./dnsproxy -l 127.0.0.1 --https-port=443 --http3 --tls-crt=example.crt --tls-key=example.key -u 8.8.8.8:53 -p 0
```
Runs a DNS-over-QUIC proxy on `127.0.0.1:853`.
```shell
./dnsproxy -l 127.0.0.1 --quic-port=853 --tls-crt=example.crt --tls-key=example.key -u 8.8.8.8:53 -p 0
Expand Down
3 changes: 1 addition & 2 deletions fastip/fastest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
6 changes: 3 additions & 3 deletions fastip/ping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ type Options struct {
DNSCryptConfigPath string `yaml:"dnscrypt-config" short:"g" long:"dnscrypt-config" description:"Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt"`

// HTTP3 controls whether HTTP/3 is enabled for this instance of dnsproxy.
// At this point it only enables it for upstreams, but in the future it will
// also enable it for the server.
// It enables HTTP/3 support for both the DoH upstreams and the DoH server.
HTTP3 bool `yaml:"http3" long:"http3" description:"Enable HTTP/3 support" optional:"yes" optional-value:"false"`

// Upstream DNS servers settings
Expand Down Expand Up @@ -274,6 +273,7 @@ func createProxyConfig(options *Options) proxy.Config {
CacheMaxTTL: options.CacheMaxTTL,
CacheOptimistic: options.CacheOptimistic,
RefuseAny: options.RefuseAny,
HTTP3: options.HTTP3,
// TODO(e.burkov): The following CIDRs are aimed to match any
// address. This is not quite proper approach to be used by
// default so think about configuring it.
Expand Down
1 change: 1 addition & 0 deletions proxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
107 changes: 43 additions & 64 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package proxy

import (
"fmt"
"io"
"net"
"net/http"
"sync"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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
// --
Expand Down Expand Up @@ -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{} {
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down
8 changes: 2 additions & 6 deletions proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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{}
Expand Down
9 changes: 7 additions & 2 deletions proxy/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/AdguardTeam/golibs/log"
"github.com/lucas-clemente/quic-go"
"github.com/miekg/dns"
)

Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 22 additions & 1 deletion proxy/server_dnscrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit a03a56c

Please sign in to comment.