mirror of
https://github.com/AdguardTeam/dnsproxy.git
synced 2026-10-12 02:01:26 +00:00
Pull request 294: all: support basic-auth-only doh mode
Closes #359.Closes #360. Squashed commit of the following: commit 26af6d91498087ac831c8eb61b1a54a40107a7e1 Author: Ainar Garipov <[email protected]> Date: Thu Nov 2 15:08:56 2023 +0300 all: redo basic auth commit 753fd688ee6d35fb258dd9f9c2fa572c4e07eb75 Author: Ainar Garipov <[email protected]> Date: Wed Nov 1 18:01:00 2023 +0300 all: imp docs, names commit 2c1f47690f58d0dabba40f29ae273d688d6a35f8 Author: Ainar Garipov <[email protected]> Date: Wed Nov 1 17:27:41 2023 +0300 all: support basic-auth-only doh mode
This commit is contained in:
@@ -86,6 +86,7 @@ Application Options:
|
||||
--dns64 If specified, dnsproxy will act as a DNS64 server
|
||||
--dns64-prefix= Prefix used to handle DNS64. If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b::. Can be specified multiple times
|
||||
--https-server-name= Set the Server header for the responses from the HTTPS server. (default: dnsproxy)
|
||||
--https-userinfo= If set, all DoH queries are required to have this basic authentication information.
|
||||
--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.
|
||||
--udp-buf-size= Set the size of the UDP buffer in bytes. A value <= 0 will use the system default.
|
||||
@@ -349,3 +350,26 @@ instead of responses containing any IP from `192.168.0.0`-`192.168.255.255`:
|
||||
```
|
||||
./dnsproxy -u 192.168.0.15:53 --bogus-nxdomain=192.168.0.0/16
|
||||
```
|
||||
|
||||
### Basic Auth for DoH
|
||||
|
||||
By setting the `--https-userinfo` option you can use `dnsproxy` as a DoH proxy
|
||||
with basic authentication requirements.
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
./dnsproxy\
|
||||
--https-port='443'\
|
||||
--https-userinfo='user:p4ssw0rd'\
|
||||
--tls-crt='…/my.crt'\
|
||||
--tls-key='…/my.key'\
|
||||
-u '94.140.14.14:53'
|
||||
```
|
||||
|
||||
This configuration will only allow DoH queries that contain an `Authorization`
|
||||
header containing the BasicAuth credentials for user `user` with password
|
||||
`p4ssw0rd`.
|
||||
|
||||
Add `-p 0` if you also want to disable plain-DNS handling and make `dnsproxy`
|
||||
only serve DoH with Basic Auth checking.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
@@ -180,6 +181,11 @@ type Options struct {
|
||||
// Set Server header for the HTTPS server
|
||||
HTTPSServerName string `yaml:"https-server-name" long:"https-server-name" description:"Set the Server header for the responses from the HTTPS server." default:"dnsproxy"`
|
||||
|
||||
// HTTPSUserinfo is the sole permitted userinfo for the DoH basic
|
||||
// authentication. If it is set, all DoH queries are required to have this
|
||||
// basic authentication information.
|
||||
HTTPSUserinfo string `yaml:"https-userinfo" long:"https-userinfo" description:"If set, all DoH queries are required to have this basic authentication information."`
|
||||
|
||||
// If true, all AAAA requests will be replied with NoError RCode and empty answer
|
||||
IPv6Disabled bool `yaml:"ipv6-disabled" long:"ipv6-disabled" description:"If specified, all AAAA requests will be replied with NoError RCode and empty answer" optional:"yes" optional-value:"true"`
|
||||
|
||||
@@ -267,8 +273,8 @@ func run(options *Options) {
|
||||
log.Info("Starting dnsproxy %s", version.Version())
|
||||
|
||||
// Prepare the proxy server and its configuration.
|
||||
config := createProxyConfig(options)
|
||||
dnsProxy := &proxy.Proxy{Config: config}
|
||||
conf := createProxyConfig(options)
|
||||
dnsProxy := &proxy.Proxy{Config: conf}
|
||||
|
||||
// Add extra handler if needed.
|
||||
if options.IPv6Disabled {
|
||||
@@ -325,9 +331,8 @@ func runPprof(options *Options) {
|
||||
}
|
||||
|
||||
// createProxyConfig creates proxy.Config from the command line arguments
|
||||
func createProxyConfig(options *Options) proxy.Config {
|
||||
// Create the config
|
||||
config := proxy.Config{
|
||||
func createProxyConfig(options *Options) (conf proxy.Config) {
|
||||
conf = proxy.Config{
|
||||
RatelimitSubnetMaskIPv4: net.CIDRMask(options.RatelimitSubnetLenIPv4, netutil.IPv4BitLen),
|
||||
RatelimitSubnetMaskIPv6: net.CIDRMask(options.RatelimitSubnetLenIPv6, netutil.IPv6BitLen),
|
||||
|
||||
@@ -349,16 +354,25 @@ func createProxyConfig(options *Options) proxy.Config {
|
||||
MaxGoroutines: options.MaxGoRoutines,
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Make these methods of [Options].
|
||||
initUpstreams(&config, options)
|
||||
initEDNS(&config, options)
|
||||
initBogusNXDomain(&config, options)
|
||||
initTLSConfig(&config, options)
|
||||
initDNSCryptConfig(&config, options)
|
||||
initListenAddrs(&config, options)
|
||||
initDNS64(&config, options)
|
||||
if uiStr := options.HTTPSUserinfo; uiStr != "" {
|
||||
user, pass, ok := strings.Cut(uiStr, ":")
|
||||
if ok {
|
||||
conf.Userinfo = url.UserPassword(user, pass)
|
||||
} else {
|
||||
conf.Userinfo = url.User(user)
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
// TODO(e.burkov): Make these methods of [Options].
|
||||
initUpstreams(&conf, options)
|
||||
initEDNS(&conf, options)
|
||||
initBogusNXDomain(&conf, options)
|
||||
initTLSConfig(&conf, options)
|
||||
initDNSCryptConfig(&conf, options)
|
||||
initListenAddrs(&conf, options)
|
||||
initDNS64(&conf, options)
|
||||
|
||||
return conf
|
||||
}
|
||||
|
||||
// isEmpty returns false if uc contains at least a single upstream. uc must not
|
||||
|
||||
+7
-1
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
@@ -162,6 +163,11 @@ type Config struct {
|
||||
// not empty.
|
||||
HTTPSServerName string
|
||||
|
||||
// Userinfo is the sole permitted userinfo for the DoH basic authentication.
|
||||
// If Userinfo is set, all DoH queries are required to have this basic
|
||||
// authentication information.
|
||||
Userinfo *url.Userinfo
|
||||
|
||||
// MaxGoroutines is the maximum number of goroutines processing DNS
|
||||
// requests. Important for mobile users.
|
||||
//
|
||||
@@ -201,7 +207,7 @@ func (p *Proxy) validateConfig() error {
|
||||
|
||||
err = p.UpstreamConfig.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating general usptreams: %w", err)
|
||||
return fmt.Errorf("validating general upstreams: %w", err)
|
||||
}
|
||||
|
||||
// Allow both [Proxy.PrivateRDNSUpstreamConfig] and [Proxy.Fallbacks] to be
|
||||
|
||||
+17
-13
@@ -39,12 +39,13 @@ func (p *Proxy) exchange(req *dns.Msg, upstreams []upstream.Upstream) (reply *dn
|
||||
var elapsed int
|
||||
reply, elapsed, err = exchangeWithUpstream(dnsUpstream, req)
|
||||
if err == nil {
|
||||
p.updateRtt(dnsUpstream.Address(), elapsed)
|
||||
p.updateRTT(dnsUpstream.Address(), elapsed)
|
||||
|
||||
return reply, dnsUpstream, err
|
||||
}
|
||||
|
||||
errs = append(errs, err)
|
||||
p.updateRtt(dnsUpstream.Address(), int(defaultTimeout/time.Millisecond))
|
||||
p.updateRTT(dnsUpstream.Address(), int(defaultTimeout/time.Millisecond))
|
||||
}
|
||||
|
||||
return nil, nil, errors.List("all upstreams failed to exchange request", errs...)
|
||||
@@ -59,7 +60,7 @@ func (p *Proxy) getSortedUpstreams(u []upstream.Upstream) []upstream.Upstream {
|
||||
|
||||
slices.SortFunc(clone, func(a, b upstream.Upstream) (res int) {
|
||||
// TODO(d.kolyshev): Use upstreams for sort comparing.
|
||||
return p.upstreamRttStats[a.Address()] - p.upstreamRttStats[b.Address()]
|
||||
return p.upstreamRTTStats[a.Address()] - p.upstreamRTTStats[b.Address()]
|
||||
})
|
||||
|
||||
return clone
|
||||
@@ -70,18 +71,20 @@ func exchangeWithUpstream(u upstream.Upstream, req *dns.Msg) (*dns.Msg, int, err
|
||||
startTime := time.Now()
|
||||
reply, err := u.Exchange(req)
|
||||
elapsed := time.Since(startTime)
|
||||
|
||||
addr := u.Address()
|
||||
if err != nil {
|
||||
log.Error(
|
||||
"upstream %s failed to exchange %s in %s. Cause: %s",
|
||||
u.Address(),
|
||||
"dnsproxy: upstream %s failed to exchange %s in %s: %s",
|
||||
addr,
|
||||
req.Question[0].String(),
|
||||
elapsed,
|
||||
err,
|
||||
)
|
||||
} else {
|
||||
log.Tracef(
|
||||
"upstream %s successfully finished exchange of %s. Elapsed %s.",
|
||||
u.Address(),
|
||||
log.Debug(
|
||||
"dnsproxy: upstream %s successfully finished exchange of %s; elapsed %s",
|
||||
addr,
|
||||
req.Question[0].String(),
|
||||
elapsed,
|
||||
)
|
||||
@@ -90,13 +93,14 @@ func exchangeWithUpstream(u upstream.Upstream, req *dns.Msg) (*dns.Msg, int, err
|
||||
return reply, int(elapsed.Milliseconds()), err
|
||||
}
|
||||
|
||||
// updateRtt updates rtt in upstreamRttStats for given address
|
||||
func (p *Proxy) updateRtt(address string, rtt int) {
|
||||
// updateRTT updates the round-trip time in upstreamRTTStats for given address.
|
||||
func (p *Proxy) updateRTT(address string, rtt int) {
|
||||
p.rttLock.Lock()
|
||||
defer p.rttLock.Unlock()
|
||||
|
||||
if p.upstreamRttStats == nil {
|
||||
p.upstreamRttStats = map[string]int{}
|
||||
if p.upstreamRTTStats == nil {
|
||||
p.upstreamRTTStats = map[string]int{}
|
||||
}
|
||||
p.upstreamRttStats[address] = (p.upstreamRttStats[address] + rtt) / 2
|
||||
|
||||
p.upstreamRTTStats[address] = (p.upstreamRTTStats[address] + rtt) / 2
|
||||
}
|
||||
|
||||
+25
-3
@@ -108,11 +108,11 @@ type Proxy struct {
|
||||
// Upstream
|
||||
// --
|
||||
|
||||
// upstreamRttStats is a map of upstream addresses and their rtt. Used to
|
||||
// upstreamRTTStats is a map of upstream addresses and their rtt. Used to
|
||||
// sort upstreams by their latency.
|
||||
upstreamRttStats map[string]int
|
||||
upstreamRTTStats map[string]int
|
||||
|
||||
// rttLock protects upstreamRttStats.
|
||||
// rttLock protects upstreamRTTStats.
|
||||
rttLock sync.Mutex
|
||||
|
||||
// DNS64 (in case dnsproxy works in a NAT64/DNS64 network)
|
||||
@@ -174,11 +174,18 @@ type Proxy struct {
|
||||
requestGoroutinesSema semaphore
|
||||
|
||||
// Config is the proxy configuration.
|
||||
//
|
||||
// TODO(a.garipov): Remove this embed and create a proper initializer.
|
||||
Config
|
||||
}
|
||||
|
||||
// Init populates fields of p but does not start listeners.
|
||||
func (p *Proxy) Init() (err error) {
|
||||
err = p.validateBasicAuth()
|
||||
if err != nil {
|
||||
return fmt.Errorf("basic auth: %w", err)
|
||||
}
|
||||
|
||||
p.initCache()
|
||||
|
||||
if p.MaxGoroutines > 0 {
|
||||
@@ -227,6 +234,21 @@ func (p *Proxy) Init() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateBasicAuth validates the basic-auth mode settings if p.Config.Userinfo
|
||||
// is set.
|
||||
func (p *Proxy) validateBasicAuth() (err error) {
|
||||
conf := p.Config
|
||||
if conf.Userinfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(conf.HTTPSListenAddr) == 0 {
|
||||
return errors.Error("no https addrs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start initializes the proxy server and starts listening
|
||||
func (p *Proxy) Start() (err error) {
|
||||
log.Info("dnsproxy: starting dns proxy server")
|
||||
|
||||
+5
-6
@@ -323,12 +323,11 @@ func TestUpstreamsSort(t *testing.T) {
|
||||
upstreams = append(upstreams, up)
|
||||
}
|
||||
|
||||
// create upstreamRttStats for 3 upstreams
|
||||
upstreamRttStats := map[string]int{}
|
||||
upstreamRttStats["1.1.1.1:53"] = 10
|
||||
upstreamRttStats["2.3.4.5:53"] = 20
|
||||
upstreamRttStats["1.2.3.4:53"] = 30
|
||||
testProxy.upstreamRttStats = upstreamRttStats
|
||||
upstreamRTTStats := map[string]int{}
|
||||
upstreamRTTStats["1.1.1.1:53"] = 10
|
||||
upstreamRTTStats["2.3.4.5:53"] = 20
|
||||
upstreamRTTStats["1.2.3.4:53"] = 30
|
||||
testProxy.upstreamRTTStats = upstreamRTTStats
|
||||
|
||||
sortedUpstreams := testProxy.getSortedUpstreams(upstreams)
|
||||
|
||||
|
||||
+57
-16
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -98,17 +99,25 @@ func (p *Proxy) createHTTPSListeners() (err error) {
|
||||
// "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)
|
||||
log.Debug("dnsproxy: incoming https request on %s", r.URL)
|
||||
|
||||
raddr, prx, err := remoteAddr(r)
|
||||
if err != nil {
|
||||
log.Debug("dnsproxy: warning: getting real ip: %s", err)
|
||||
}
|
||||
|
||||
if !p.checkBasicAuth(w, r, raddr) {
|
||||
return
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
var err error
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
dnsParam := r.URL.Query().Get("dns")
|
||||
buf, err = base64.RawURLEncoding.DecodeString(dnsParam)
|
||||
if len(buf) == 0 || err != nil {
|
||||
log.Tracef("Cannot parse DNS request from %s", dnsParam)
|
||||
log.Debug("dnsproxy: parsing dns request from get param %q: %v", dnsParam, err)
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
@@ -116,7 +125,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodPost:
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/dns-message" {
|
||||
log.Tracef("Unsupported media type: %s", contentType)
|
||||
log.Debug("dnsproxy: unsupported media type %q", contentType)
|
||||
http.Error(w, http.StatusText(http.StatusUnsupportedMediaType), http.StatusUnsupportedMediaType)
|
||||
|
||||
return
|
||||
@@ -124,14 +133,15 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
buf, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Tracef("Cannot read the request body: %s", err)
|
||||
log.Debug("dnsproxy: reading http request body: %s", err)
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer log.OnCloserError(r.Body, log.DEBUG)
|
||||
default:
|
||||
log.Tracef("Wrong HTTP method: %s", r.Method)
|
||||
log.Debug("dnsproxy: bad http method %q", r.Method)
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
|
||||
return
|
||||
@@ -139,37 +149,68 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
req := &dns.Msg{}
|
||||
if err = req.Unpack(buf); err != nil {
|
||||
log.Tracef("msg.Unpack: %s", err)
|
||||
log.Debug("dnsproxy: unpacking http msg: %s", err)
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
addr, prx, err := remoteAddr(r)
|
||||
if err != nil {
|
||||
log.Debug("warning: getting real ip: %s", err)
|
||||
}
|
||||
|
||||
d := p.newDNSContext(ProtoHTTPS, req)
|
||||
d.Addr = addr
|
||||
d.Addr = raddr
|
||||
d.HTTPRequest = r
|
||||
d.HTTPResponseWriter = w
|
||||
|
||||
if prx != nil {
|
||||
ip, _ := netutil.IPAndPortFromAddr(prx)
|
||||
log.Debug("request came from proxy server %s", prx)
|
||||
log.Debug("dnsproxy: request came from proxy server %s", prx)
|
||||
if !p.proxyVerifier.Contains(ip) {
|
||||
log.Debug("proxy %s is not trusted, using original remote addr", ip)
|
||||
log.Debug("dnsproxy: proxy %s is not trusted, using original remote addr", ip)
|
||||
d.Addr = prx
|
||||
}
|
||||
}
|
||||
|
||||
err = p.handleDNSRequest(d)
|
||||
if err != nil {
|
||||
log.Tracef("error handling DNS (%s) request: %s", d.Proto, err)
|
||||
log.Debug("dnsproxy: handling dns (%s) request: %s", d.Proto, err)
|
||||
}
|
||||
}
|
||||
|
||||
// checkBasicAuth checks the basic authorization data, if necessary, and if the
|
||||
// data isn't valid, it writes an error. shouldHandle is false if the request
|
||||
// has been denied.
|
||||
func (p *Proxy) checkBasicAuth(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
raddr net.Addr,
|
||||
) (shouldHandle bool) {
|
||||
ui := p.Config.Userinfo
|
||||
if ui == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
user, pass, _ := r.BasicAuth()
|
||||
if matchesUserinfo(ui, user, pass) {
|
||||
return true
|
||||
}
|
||||
|
||||
log.Error("dnsproxy: basic auth failed for user %q from raddr %s", user, raddr)
|
||||
|
||||
h := w.Header()
|
||||
// TODO(a.garipov): Add to httphdr.
|
||||
h.Set("Www-Authenticate", `Basic realm="DNS", charset="UTF-8"`)
|
||||
http.Error(w, "Authorization required", http.StatusUnauthorized)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesUserinfo returns false if user and pass don't match userinfo.
|
||||
// userinfo must not be nil.
|
||||
func matchesUserinfo(userinfo *url.Userinfo, user, pass string) (ok bool) {
|
||||
requiredPassword, _ := userinfo.Password()
|
||||
|
||||
return user == userinfo.Username() && pass == requiredPassword
|
||||
}
|
||||
|
||||
// Writes a response to the DoH client.
|
||||
func (p *Proxy) respondHTTPS(d *DNSContext) (err error) {
|
||||
resp := d.Res
|
||||
|
||||
+5
-8
@@ -231,18 +231,15 @@ func (p *configParser) specifyUpstream(
|
||||
p.upstreamsIndex[u] = dnsUpstream
|
||||
}
|
||||
|
||||
addr := dnsUpstream.Address()
|
||||
if len(domains) == 0 {
|
||||
log.Debug("Upstream %d: %s", idx, dnsUpstream.Address())
|
||||
log.Debug("dnsproxy: upstream at index %d: %s", idx, addr)
|
||||
p.upstreams = append(p.upstreams, dnsUpstream)
|
||||
|
||||
return nil
|
||||
} else {
|
||||
log.Debug("dnsproxy: upstream at index %d: %s is reserved for %s", idx, addr, domains)
|
||||
p.includeToReserved(dnsUpstream, domains)
|
||||
}
|
||||
|
||||
p.includeToReserved(dnsUpstream, domains)
|
||||
|
||||
log.Debug("Upstream %d: %s is reserved for next domains: %s",
|
||||
idx, dnsUpstream.Address(), domains)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# This comment is used to simplify checking local copies of the staticcheck
|
||||
# configuration. Bump this number every time a significant change is made to
|
||||
# this file.
|
||||
#
|
||||
# AdGuard-Project-Version: 1
|
||||
checks = ["all"]
|
||||
initialisms = [
|
||||
# See https://github.com/dominikh/go-tools/blob/master/config/config.go.
|
||||
#
|
||||
# Do not add "PTR" since we use "Ptr" as a suffix.
|
||||
"inherit"
|
||||
, "ASN"
|
||||
, "DHCP"
|
||||
, "DNSSEC"
|
||||
# E.g. SentryDSN.
|
||||
, "DSN"
|
||||
, "ECS"
|
||||
, "EDNS"
|
||||
, "MX"
|
||||
, "QUIC"
|
||||
, "RA"
|
||||
, "RRSIG"
|
||||
, "RTT"
|
||||
, "SDNS"
|
||||
, "SLAAC"
|
||||
, "SOA"
|
||||
, "SVCB"
|
||||
, "TLD"
|
||||
, "WHOIS"
|
||||
]
|
||||
dot_import_whitelist = []
|
||||
http_status_code_whitelist = []
|
||||
+45
-33
@@ -63,23 +63,24 @@ type dnsOverHTTPS struct {
|
||||
// Clients are safe for concurrent use by multiple goroutines.
|
||||
client *http.Client
|
||||
|
||||
// quicConfig is the QUIC configuration that is used if HTTP/3 is enabled
|
||||
// for this upstream.
|
||||
quicConfig *quic.Config
|
||||
|
||||
// clientMu protects client.
|
||||
clientMu sync.Mutex
|
||||
clientMu *sync.Mutex
|
||||
|
||||
// quicConfMu protects quicConfig.
|
||||
quicConfMu sync.Mutex
|
||||
// quicConf is the QUIC configuration that is used if HTTP/3 is enabled
|
||||
// for this upstream.
|
||||
quicConf *quic.Config
|
||||
|
||||
// quicConfMu protects quicConf.
|
||||
quicConfMu *sync.Mutex
|
||||
|
||||
// addrRedacted is the redacted string representation of addr. It is saved
|
||||
// separately to reduce allocations during logging and error reporting.
|
||||
addrRedacted string
|
||||
|
||||
// timeout is used in HTTP client and for H3 probes.
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Upstream = (*dnsOverHTTPS)(nil)
|
||||
|
||||
// newDoH returns the DNS-over-HTTPS Upstream.
|
||||
func newDoH(addr *url.URL, opts *Options) (u Upstream, err error) {
|
||||
addPort(addr, defaultPortDoH)
|
||||
@@ -102,11 +103,12 @@ func newDoH(addr *url.URL, opts *Options) (u Upstream, err error) {
|
||||
getDialer: getDialer,
|
||||
closeBoot: closeBoot,
|
||||
addr: addr,
|
||||
quicConfig: &quic.Config{
|
||||
quicConf: &quic.Config{
|
||||
KeepAlivePeriod: QUICKeepAlivePeriod,
|
||||
TokenStore: newQUICTokenStore(),
|
||||
Tracer: opts.QUICTracer,
|
||||
},
|
||||
quicConfMu: &sync.Mutex{},
|
||||
// #nosec G402 -- TLS certificate verification could be disabled by
|
||||
// configuration.
|
||||
tlsConf: &tls.Config{
|
||||
@@ -122,7 +124,9 @@ func newDoH(addr *url.URL, opts *Options) (u Upstream, err error) {
|
||||
VerifyPeerCertificate: opts.VerifyServerCertificate,
|
||||
VerifyConnection: opts.VerifyConnection,
|
||||
},
|
||||
timeout: opts.Timeout,
|
||||
clientMu: &sync.Mutex{},
|
||||
addrRedacted: addr.Redacted(),
|
||||
timeout: opts.Timeout,
|
||||
}
|
||||
for _, v := range httpVersions {
|
||||
ups.tlsConf.NextProtos = append(ups.tlsConf.NextProtos, string(v))
|
||||
@@ -133,8 +137,13 @@ func newDoH(addr *url.URL, opts *Options) (u Upstream, err error) {
|
||||
return ups, nil
|
||||
}
|
||||
|
||||
// Address implements the [Upstream] interface for *dnsOverHTTPS.
|
||||
func (p *dnsOverHTTPS) Address() string { return p.addr.String() }
|
||||
// type check
|
||||
var _ Upstream = (*dnsOverHTTPS)(nil)
|
||||
|
||||
// Address implements the [Upstream] interface for *dnsOverHTTPS. The address
|
||||
// is redacted: if the original URL of this upstream contains a userinfo with a
|
||||
// password, the password is replaced with "xxxxx".
|
||||
func (p *dnsOverHTTPS) Address() string { return p.addrRedacted }
|
||||
|
||||
// Exchange implements the Upstream interface for *dnsOverHTTPS.
|
||||
func (p *dnsOverHTTPS) Exchange(m *dns.Msg) (resp *dns.Msg, err error) {
|
||||
@@ -215,16 +224,14 @@ func (p *dnsOverHTTPS) closeClient(client *http.Client) (err error) {
|
||||
|
||||
// exchangeHTTPS logs the request and its result and calls exchangeHTTPSClient.
|
||||
func (p *dnsOverHTTPS) exchangeHTTPS(client *http.Client, req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
addr := p.Address()
|
||||
|
||||
n := networkTCP
|
||||
if isHTTP3(client) {
|
||||
n = networkUDP
|
||||
}
|
||||
|
||||
logBegin(addr, n, req)
|
||||
logBegin(p.addrRedacted, n, req)
|
||||
resp, err = p.exchangeHTTPSClient(client, req)
|
||||
logFinish(addr, n, err)
|
||||
logFinish(p.addrRedacted, n, err)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -248,16 +255,21 @@ func (p *dnsOverHTTPS) exchangeHTTPSClient(
|
||||
method = http3.MethodGet0RTT
|
||||
}
|
||||
|
||||
q := url.Values{
|
||||
"dns": []string{base64.RawURLEncoding.EncodeToString(buf)},
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: p.addr.Scheme,
|
||||
User: p.addr.User,
|
||||
Host: p.addr.Host,
|
||||
Path: p.addr.Path,
|
||||
RawQuery: fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)),
|
||||
RawQuery: q.Encode(),
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest(method, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating http request to %s: %w", p.addr, err)
|
||||
return nil, fmt.Errorf("creating http request to %s: %w", p.addrRedacted, err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Accept", "application/dns-message")
|
||||
@@ -265,13 +277,13 @@ func (p *dnsOverHTTPS) exchangeHTTPSClient(
|
||||
|
||||
httpResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("requesting %s: %w", p.addr, err)
|
||||
return nil, fmt.Errorf("requesting %s: %w", p.addrRedacted, err)
|
||||
}
|
||||
defer log.OnCloserError(httpResp.Body, log.DEBUG)
|
||||
|
||||
body, err := io.ReadAll(httpResp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s: %w", p.addr, err)
|
||||
return nil, fmt.Errorf("reading %s: %w", p.addrRedacted, err)
|
||||
}
|
||||
|
||||
if httpResp.StatusCode != http.StatusOK {
|
||||
@@ -280,7 +292,7 @@ func (p *dnsOverHTTPS) exchangeHTTPSClient(
|
||||
"expected status %d, got %d from %s",
|
||||
http.StatusOK,
|
||||
httpResp.StatusCode,
|
||||
p.addr,
|
||||
p.addrRedacted,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -289,7 +301,7 @@ func (p *dnsOverHTTPS) exchangeHTTPSClient(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"unpacking response from %s: body is %s: %w",
|
||||
p.addr,
|
||||
p.addrRedacted,
|
||||
body,
|
||||
err,
|
||||
)
|
||||
@@ -358,7 +370,7 @@ func (p *dnsOverHTTPS) getQUICConfig() (c *quic.Config) {
|
||||
p.quicConfMu.Lock()
|
||||
defer p.quicConfMu.Unlock()
|
||||
|
||||
return p.quicConfig
|
||||
return p.quicConf
|
||||
}
|
||||
|
||||
// resetQUICConfig Re-create the token store to make sure we're not trying to
|
||||
@@ -367,8 +379,8 @@ func (p *dnsOverHTTPS) resetQUICConfig() {
|
||||
p.quicConfMu.Lock()
|
||||
defer p.quicConfMu.Unlock()
|
||||
|
||||
p.quicConfig = p.quicConfig.Clone()
|
||||
p.quicConfig.TokenStore = newQUICTokenStore()
|
||||
p.quicConf = p.quicConf.Clone()
|
||||
p.quicConf.TokenStore = newQUICTokenStore()
|
||||
}
|
||||
|
||||
// getClient gets or lazily initializes an HTTP client (and transport) that will
|
||||
@@ -428,7 +440,7 @@ func (p *dnsOverHTTPS) createClient() (*http.Client, error) {
|
||||
func (p *dnsOverHTTPS) createTransport() (t http.RoundTripper, err error) {
|
||||
dialContext, err := p.getDialer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bootstrapping %s: %w", p.addr, err)
|
||||
return nil, fmt.Errorf("bootstrapping %s: %w", p.addrRedacted, err)
|
||||
}
|
||||
|
||||
// First, we attempt to create an HTTP3 transport. If the probe QUIC
|
||||
@@ -580,7 +592,7 @@ func (p *dnsOverHTTPS) probeH3(
|
||||
|
||||
udpConn, ok := rawConn.(*net.UDPConn)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("not a UDP connection to %s", p.addr)
|
||||
return "", fmt.Errorf("not a UDP connection to %s", p.addrRedacted)
|
||||
}
|
||||
|
||||
addr = udpConn.RemoteAddr().String()
|
||||
@@ -604,13 +616,13 @@ func (p *dnsOverHTTPS) probeH3(
|
||||
probeTLSCfg.VerifyConnection = nil
|
||||
|
||||
// Run probeQUIC and probeTLS in parallel and see which one is faster.
|
||||
chQuic := make(chan error, 1)
|
||||
chQUIC := make(chan error, 1)
|
||||
chTLS := make(chan error, 1)
|
||||
go p.probeQUIC(addr, probeTLSCfg, chQuic)
|
||||
go p.probeQUIC(addr, probeTLSCfg, chQUIC)
|
||||
go p.probeTLS(dialContext, probeTLSCfg, chTLS)
|
||||
|
||||
select {
|
||||
case quicErr := <-chQuic:
|
||||
case quicErr := <-chQUIC:
|
||||
if quicErr != nil {
|
||||
// QUIC failed, return error since HTTP3 was not preferred.
|
||||
return "", quicErr
|
||||
@@ -643,7 +655,7 @@ func (p *dnsOverHTTPS) probeQUIC(addr string, tlsConfig *tls.Config, ch chan err
|
||||
|
||||
conn, err := quic.DialAddrEarly(ctx, addr, tlsConfig, p.getQUICConfig())
|
||||
if err != nil {
|
||||
ch <- fmt.Errorf("opening QUIC connection to %s: %w", p.addr, err)
|
||||
ch <- fmt.Errorf("opening quic connection to %s: %w", p.addrRedacted, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -310,10 +310,10 @@ func TestUpstreamDoH_0RTT(t *testing.T) {
|
||||
conns := tracer.getConnectionsInfo()
|
||||
require.Len(t, conns, 2)
|
||||
|
||||
// Examine the first connection (no 0-Rtt there).
|
||||
// Examine the first connection (no 0-RTT there).
|
||||
require.False(t, conns[0].is0RTT())
|
||||
|
||||
// Examine the second connection (the one that used 0-Rtt).
|
||||
// Examine the second connection (the one that used 0-RTT).
|
||||
require.True(t, conns[1].is0RTT())
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ type ExchangeAllResult struct {
|
||||
Upstream Upstream
|
||||
}
|
||||
|
||||
// ExchangeAll retunrs the responses from all of u. It returns an error only if
|
||||
// ExchangeAll returns the responses from all of u. It returns an error only if
|
||||
// all upstreams failed to exchange the request.
|
||||
func ExchangeAll(ups []Upstream, req *dns.Msg) (res []ExchangeAllResult, err error) {
|
||||
upsl := len(ups)
|
||||
@@ -159,9 +159,9 @@ func exchangeAndLog(u Upstream, req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if q := &req.Question[0]; err == nil {
|
||||
log.Debug("upstream %s exchanged %s successfully in %s", addr, q, elapsed)
|
||||
log.Debug("dnsproxy: upstream %s exchanged %s successfully in %s", addr, q, elapsed)
|
||||
} else {
|
||||
log.Debug("upstream %s failed to exchange %s in %s: %s", addr, q, elapsed, err)
|
||||
log.Debug("dnsproxy: upstream %s failed to exchange %s in %s: %s", addr, q, elapsed, err)
|
||||
}
|
||||
|
||||
return reply, err
|
||||
|
||||
@@ -157,10 +157,10 @@ func TestUpstreamDoQ_0RTT(t *testing.T) {
|
||||
conns := tracer.getConnectionsInfo()
|
||||
require.Len(t, conns, 2)
|
||||
|
||||
// Examine the first connection (no 0-Rtt there).
|
||||
// Examine the first connection (no 0-RTT there).
|
||||
require.False(t, conns[0].is0RTT())
|
||||
|
||||
// Examine the second connection (the one that used 0-Rtt).
|
||||
// Examine the second connection (the one that used 0-RTT).
|
||||
require.True(t, conns[1].is0RTT())
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ import (
|
||||
type Upstream interface {
|
||||
// Exchange sends the DNS query req to this upstream and returns the
|
||||
// response that has been received or an error if something went wrong.
|
||||
Exchange(req *dns.Msg) (*dns.Msg, error)
|
||||
Exchange(req *dns.Msg) (resp *dns.Msg, err error)
|
||||
|
||||
// Address returns the address of the upstream DNS resolver.
|
||||
Address() string
|
||||
Address() (addr string)
|
||||
|
||||
// Closer used to close the upstreams properly. Exchange shouldn't be
|
||||
// called after calling Close.
|
||||
@@ -285,23 +285,25 @@ func addPort(u *url.URL, port uint16) {
|
||||
// logBegin logs the start of DNS request resolution. It should be called right
|
||||
// before dialing the connection to the upstream. n is the [network] that will
|
||||
// be used to send the request.
|
||||
func logBegin(upstreamAddress string, n network, req *dns.Msg) {
|
||||
func logBegin(upsAddr string, n network, req *dns.Msg) {
|
||||
qtype := ""
|
||||
target := ""
|
||||
if len(req.Question) != 0 {
|
||||
qtype = dns.Type(req.Question[0].Qtype).String()
|
||||
target = req.Question[0].Name
|
||||
}
|
||||
log.Debug("%s: sending request over %s: %s %s", upstreamAddress, n, qtype, target)
|
||||
|
||||
log.Debug("dnsproxy: %s: sending request over %s: %s %s", upsAddr, n, qtype, target)
|
||||
}
|
||||
|
||||
// Write to log about the result of DNS request
|
||||
func logFinish(upstreamAddress string, n network, err error) {
|
||||
func logFinish(upsAddr string, n network, err error) {
|
||||
status := "ok"
|
||||
if err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
log.Debug("%s: response received over %s: %s", upstreamAddress, n, status)
|
||||
|
||||
log.Debug("dnsproxy: %s: response received over %s: %q", upsAddr, n, status)
|
||||
}
|
||||
|
||||
// DialerInitializer returns the handler that it creates. All the subsequent
|
||||
|
||||
Reference in New Issue
Block a user