Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add DoH3 #214

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect
github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ github.com/lucas-clemente/quic-go v0.24.0 h1:ToR7SIIEdrgOhgVTHvPgdVRJfgVy+N0wQAa
github.com/lucas-clemente/quic-go v0.24.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
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-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco=
Expand Down
2 changes: 2 additions & 0 deletions upstream/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ func urlToUpstream(uu *url.URL, opts *Options) (u Upstream, err error) {
return newDoT(uu, opts)
case "https":
return newDoH(uu, opts)
case "h3":
return newDoH3(uu, opts)
default:
return nil, fmt.Errorf("unsupported url scheme: %s", sch)
}
Expand Down
200 changes: 200 additions & 0 deletions upstream/upstream_doh3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package upstream

import (
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"sync"
"time"

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

// dnsOverHTTP3 represents DNS-over-HTTP/3 upstream.
type dnsOverHTTP3 struct {
boot *bootstrapper

client *http.Client
clientGuard sync.Mutex
}

// type check
var _ Upstream = &dnsOverHTTP3{}

// newDoH3 returns the DNS-over-HTTP3 Upstream.
func newDoH3(uu *url.URL, opts *Options) (u Upstream, err error) {
addPort(uu, defaultPortDoH)

var b *bootstrapper
b, err = urlToBoot(uu, opts)
if err != nil {
return nil, fmt.Errorf("creating https bootstrapper: %w", err)
}

return &dnsOverHTTP3{boot: b}, nil
}

func (p *dnsOverHTTP3) Address() string { return p.boot.URL.String() }

func (p *dnsOverHTTP3) Exchange(m *dns.Msg) (*dns.Msg, error) {
client, err := p.getClient()
if err != nil {
return nil, fmt.Errorf("initializing http client: %w", err)
}

logBegin(p.Address(), m)
r, err := p.exchangeHTTP3Client(m, client)
logFinish(p.Address(), err)

return r, err
}

// exchangeHTTP3Client sends the DNS query to a DOH3 resolver using the specified
// http.Client instance.
func (p *dnsOverHTTP3) exchangeHTTP3Client(m *dns.Msg, client *http.Client) (*dns.Msg, error) {
buf, err := m.Pack()
if err != nil {
return nil, fmt.Errorf("packing message: %w", err)
}

requestURL := p.Address() + "?dns=" + base64.RawURLEncoding.EncodeToString(buf)
u, err := url.Parse(requestURL)
if err != nil {
return nil, fmt.Errorf("parse request URL: %w", err)
}

u.Scheme = "https"
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("creating http request to %s: %w", p.boot.URL, err)
}

req.Header.Set("Accept", "application/dns-message")

resp, err := client.Do(req)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
// If this is a timeout error, trying to forcibly re-create the HTTP
// client instance.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3217.
p.clientGuard.Lock()
p.client = nil
p.clientGuard.Unlock()
}

return nil, fmt.Errorf("requesting %s: %w", p.boot.URL, err)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", p.boot.URL, err)
}

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("got status code %d from %s", resp.StatusCode, p.boot.URL)
}

response := dns.Msg{}
err = response.Unpack(body)
if err != nil {
return nil, fmt.Errorf("unpacking response from %s: body is %s: %w", p.boot.URL, string(body), err)
}

if response.Id != m.Id {
err = dns.ErrId
}

return &response, err
}

// getClient gets or lazily initializes an HTTP client (and transport) that will
// be used for this DOH resolver.
func (p *dnsOverHTTP3) getClient() (c *http.Client, err error) {
startTime := time.Now()

p.clientGuard.Lock()
defer p.clientGuard.Unlock()
if p.client != nil {
return p.client, nil
}

// 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)
}

p.client, err = p.createClient()

return p.client, err
}

func (p *dnsOverHTTP3) createClient() (*http.Client, error) {
transport, err := p.createTransport()
if err != nil {
return nil, fmt.Errorf("initializing http transport: %w", err)
}

client := &http.Client{
Transport: transport,
Timeout: p.boot.options.Timeout,
Jar: nil,
}

p.client = client
return p.client, nil
}

// createTransport initializes an HTTP transport that will be used specifically
// for this DOH3 resolver. This HTTP transport ensures that the HTTP requests
// will be sent exactly to the IP address got from the bootstrap resolver.
func (p *dnsOverHTTP3) createTransport() (http.RoundTripper, error) {
tlsConfig, dialContext, err := p.boot.get()
if err != nil {
return nil, fmt.Errorf("bootstrapping %s: %w", p.boot.URL, err)
}

quicConfig := &quic.Config{
HandshakeIdleTimeout: handshakeTimeout,
}

transport := &http3.RoundTripper{
DisableCompression: true,
TLSClientConfig: tlsConfig,
QuicConfig: quicConfig,
Dial: func(network, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, 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're v4/v6 addresses)
rawConn, e := dialContext(context.Background(), "udp", "")
if e != nil {
return nil, e
}
// It's never actually used
_ = rawConn.Close()

udpConn, ok := rawConn.(*net.UDPConn)
if !ok {
return nil, fmt.Errorf("failed to open connection to %s", p.Address())
}

return quic.DialAddrEarly(udpConn.RemoteAddr().String(), tlsCfg, cfg)
},
}

return transport, nil
}
30 changes: 30 additions & 0 deletions upstream/upstream_doh3_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package upstream

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
)

func TestUpstreamDOH3(t *testing.T) {
// Create a DNS-over-HTTP3 upstream
address := "h3://dns.google/dns-query"
u, err := AddressToUpstream(address, &Options{InsecureSkipVerify: true})
assert.Nil(t, err)

uq := u.(*dnsOverHTTP3)
var client *http.Client

// Test that it responds properly
for i := 0; i < 10; i++ {
checkUpstream(t, u, address)

if client == nil {
client = uq.client
} else {
// This way we test that the client is properly reused
assert.True(t, client == uq.client)
}
}
}
98 changes: 98 additions & 0 deletions vendor/github.com/lucas-clemente/quic-go/http3/body.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading