all: sync with master; upd chlog
This commit is contained in:
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
// ValidateClientID returns an error if id is not a valid ClientID.
|
||||
@@ -151,25 +151,7 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
||||
func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string, err error) {
|
||||
switch proto {
|
||||
case proxy.ProtoHTTPS:
|
||||
// github.com/lucas-clemente/quic-go seems to not populate the TLS
|
||||
// field. So, if the request comes over HTTP/3, use the Host header
|
||||
// value as the server name.
|
||||
//
|
||||
// See https://github.com/lucas-clemente/quic-go/issues/2879.
|
||||
//
|
||||
// TODO(a.garipov): Remove this crutch once they fix it.
|
||||
r := pctx.HTTPRequest
|
||||
if r.ProtoAtLeast(3, 0) {
|
||||
var host string
|
||||
host, err = netutil.SplitHost(r.Host)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing host: %w", err)
|
||||
}
|
||||
|
||||
srvName = host
|
||||
} else if connState := r.TLS; connState != nil {
|
||||
srvName = r.TLS.ServerName
|
||||
}
|
||||
srvName = pctx.HTTPRequest.TLS.ServerName
|
||||
case proxy.ProtoQUIC:
|
||||
qConn := pctx.QUICConnection
|
||||
conn, ok := qConn.(quicConnection)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -55,7 +55,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID string
|
||||
wantErrMsg string
|
||||
strictSNI bool
|
||||
useHTTP3 bool
|
||||
}{{
|
||||
name: "udp",
|
||||
proto: proxy.ProtoUDP,
|
||||
@@ -64,7 +63,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
strictSNI: false,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_no_clientid",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -73,7 +71,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_no_client_server_name",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -83,7 +80,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantErrMsg: `clientid check: client server name "" ` +
|
||||
`doesn't match host server name "example.com"`,
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_no_client_server_name_no_strict",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -92,7 +88,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
strictSNI: false,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_clientid",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -101,7 +96,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_clientid_hostname_error",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -111,7 +105,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantErrMsg: `clientid check: client server name "cli.example.net" ` +
|
||||
`doesn't match host server name "example.com"`,
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_invalid_clientid",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -121,7 +114,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantErrMsg: `clientid check: invalid clientid "!!!": ` +
|
||||
`bad domain name label rune '!'`,
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_clientid_too_long",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -133,7 +125,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
`pqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789": ` +
|
||||
`domain name label is too long: got 72, max 63`,
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "quic_clientid",
|
||||
proto: proxy.ProtoQUIC,
|
||||
@@ -142,7 +133,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_clientid_issue3437",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -152,7 +142,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantErrMsg: `clientid check: client server name "cli.myexample.com" ` +
|
||||
`doesn't match host server name "example.com"`,
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "tls_case",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -161,7 +150,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "insensitive",
|
||||
wantErrMsg: ``,
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "quic_case",
|
||||
proto: proxy.ProtoQUIC,
|
||||
@@ -170,7 +158,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "insensitive",
|
||||
wantErrMsg: ``,
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "https_no_clientid",
|
||||
proto: proxy.ProtoHTTPS,
|
||||
@@ -179,7 +166,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "https_clientid",
|
||||
proto: proxy.ProtoHTTPS,
|
||||
@@ -188,16 +174,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
strictSNI: true,
|
||||
useHTTP3: false,
|
||||
}, {
|
||||
name: "https_clientid_quic",
|
||||
proto: proxy.ProtoHTTPS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: "cli.example.com",
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
strictSNI: true,
|
||||
useHTTP3: true,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -219,7 +195,7 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
|
||||
switch tc.proto {
|
||||
case proxy.ProtoHTTPS:
|
||||
httpReq = newHTTPReq(tc.cliSrvName, tc.useHTTP3)
|
||||
httpReq = newHTTPReq(tc.cliSrvName)
|
||||
case proxy.ProtoQUIC:
|
||||
qconn = testQUICConnection{
|
||||
serverName: tc.cliSrvName,
|
||||
@@ -246,21 +222,11 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
}
|
||||
|
||||
// newHTTPReq is a helper to create HTTP requests for tests.
|
||||
func newHTTPReq(cliSrvName string, useHTTP3 bool) (r *http.Request) {
|
||||
func newHTTPReq(cliSrvName string) (r *http.Request) {
|
||||
u := &url.URL{
|
||||
Path: "/dns-query",
|
||||
}
|
||||
|
||||
if useHTTP3 {
|
||||
return &http.Request{
|
||||
ProtoMajor: 3,
|
||||
ProtoMinor: 0,
|
||||
URL: u,
|
||||
Host: cliSrvName,
|
||||
TLS: &tls.ConnectionState{},
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Request{
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -225,7 +226,7 @@ type ServerConfig struct {
|
||||
LocalPTRResolvers []string
|
||||
|
||||
// DNS64Prefixes is a slice of NAT64 prefixes to be used for DNS64.
|
||||
DNS64Prefixes []string
|
||||
DNS64Prefixes []netip.Prefix
|
||||
|
||||
// ResolveClients signals if the RDNS should resolve clients' addresses.
|
||||
ResolveClients bool
|
||||
@@ -271,6 +272,8 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
|
||||
RequestHandler: s.handleDNSRequest,
|
||||
EnableEDNSClientSubnet: srvConf.EnableEDNSClientSubnet,
|
||||
MaxGoroutines: int(srvConf.MaxGoroutines),
|
||||
UseDNS64: srvConf.UseDNS64,
|
||||
DNS64Prefs: srvConf.DNS64Prefixes,
|
||||
}
|
||||
|
||||
if srvConf.CacheSize != 0 {
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
@@ -17,6 +19,9 @@ import (
|
||||
)
|
||||
|
||||
// To transfer information between modules
|
||||
//
|
||||
// TODO(s.chzhen): Add lowercased, non-FQDN version of the hostname from the
|
||||
// question of the request.
|
||||
type dnsContext struct {
|
||||
proxyCtx *proxy.DNSContext
|
||||
|
||||
@@ -419,7 +424,7 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
|
||||
}
|
||||
resp.Answer = append(resp.Answer, a)
|
||||
case dns.TypeAAAA:
|
||||
if len(s.dns64Prefs) > 0 {
|
||||
if s.dns64Pref != (netip.Prefix{}) {
|
||||
// Respond with DNS64-mapped address for IPv4 host if DNS64 is
|
||||
// enabled.
|
||||
aaaa := &dns.AAAA{
|
||||
@@ -468,15 +473,6 @@ func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
if s.shouldStripDNS64(ip) {
|
||||
// Strip the prefix from the address to get the original IPv4.
|
||||
ip = ip[nat64PrefixLen:]
|
||||
|
||||
// Treat a DNS64-prefixed address as a locally served one since those
|
||||
// queries should never be sent to the global DNS.
|
||||
dctx.unreversedReqIP = ip
|
||||
}
|
||||
|
||||
// Restrict an access to local addresses for external clients. We also
|
||||
// assume that all the DHCP leases we give are locally served or at least
|
||||
// shouldn't be accessible externally.
|
||||
@@ -655,13 +651,7 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
|
||||
|
||||
s.setCustomUpstream(pctx, dctx.clientID)
|
||||
|
||||
origReqAD := false
|
||||
if s.conf.EnableDNSSEC {
|
||||
origReqAD = req.AuthenticatedData
|
||||
if !req.AuthenticatedData {
|
||||
req.AuthenticatedData = true
|
||||
}
|
||||
}
|
||||
reqWantsDNSSEC := s.setReqAD(req)
|
||||
|
||||
// Process the request further since it wasn't filtered.
|
||||
prx := s.proxy()
|
||||
@@ -671,23 +661,73 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
if dctx.err = prx.Resolve(pctx); dctx.err != nil {
|
||||
return resultCodeError
|
||||
}
|
||||
if err := prx.Resolve(pctx); err != nil {
|
||||
if errors.Is(err, upstream.ErrNoUpstreams) {
|
||||
// Do not even put into querylog. Currently this happens either
|
||||
// when the private resolvers enabled and the request is DNS64 PTR,
|
||||
// or when the client isn't considered local by prx.
|
||||
//
|
||||
// TODO(e.burkov): Make proxy detect local client the same way as
|
||||
// AGH does.
|
||||
pctx.Res = s.genNXDomain(req)
|
||||
|
||||
return resultCodeFinish
|
||||
}
|
||||
|
||||
dctx.err = err
|
||||
|
||||
if s.performDNS64(prx, dctx) == resultCodeError {
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
dctx.responseFromUpstream = true
|
||||
dctx.responseAD = pctx.Res.AuthenticatedData
|
||||
|
||||
if s.conf.EnableDNSSEC && !origReqAD {
|
||||
s.setRespAD(pctx, reqWantsDNSSEC)
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// setReqAD changes the request based on the server settings. wantsDNSSEC is
|
||||
// false if the response should be cleared of the AD bit.
|
||||
//
|
||||
// TODO(a.garipov, e.burkov): This should probably be done in module dnsproxy.
|
||||
func (s *Server) setReqAD(req *dns.Msg) (wantsDNSSEC bool) {
|
||||
if !s.conf.EnableDNSSEC {
|
||||
return false
|
||||
}
|
||||
|
||||
origReqAD := req.AuthenticatedData
|
||||
req.AuthenticatedData = true
|
||||
|
||||
// Per [RFC 6840] says, validating resolvers should only set the AD bit when
|
||||
// the response has the AD bit set and the request contained either a set DO
|
||||
// bit or a set AD bit. So, if neither of these is true, clear the AD bits
|
||||
// in [Server.setRespAD].
|
||||
//
|
||||
// [RFC 6840]: https://datatracker.ietf.org/doc/html/rfc6840#section-5.8
|
||||
return origReqAD || hasDO(req)
|
||||
}
|
||||
|
||||
// hasDO returns true if msg has EDNS(0) options and the DNSSEC OK flag is set
|
||||
// in there.
|
||||
//
|
||||
// TODO(a.garipov): Move to golibs/dnsmsg when it's there.
|
||||
func hasDO(msg *dns.Msg) (do bool) {
|
||||
o := msg.IsEdns0()
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return o.Do()
|
||||
}
|
||||
|
||||
// setRespAD changes the request and response based on the server settings and
|
||||
// the original request data.
|
||||
func (s *Server) setRespAD(pctx *proxy.DNSContext, reqWantsDNSSEC bool) {
|
||||
if s.conf.EnableDNSSEC && !reqWantsDNSSEC {
|
||||
pctx.Req.AuthenticatedData = false
|
||||
pctx.Res.AuthenticatedData = false
|
||||
}
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// isDHCPClientHostQ returns true if q is from a request for a DHCP client
|
||||
|
||||
@@ -1,34 +1,10 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/mathutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxNAT64PrefixBitLen is the maximum length of a NAT64 prefix in bits.
|
||||
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.2.
|
||||
maxNAT64PrefixBitLen = 96
|
||||
|
||||
// nat64PrefixLen is the length of a NAT64 prefix in bytes.
|
||||
nat64PrefixLen = net.IPv6len - net.IPv4len
|
||||
|
||||
// maxDNS64SynTTL is the maximum TTL for synthesized DNS64 responses with no
|
||||
// SOA records in seconds.
|
||||
//
|
||||
// If the SOA RR was not delivered with the negative response to the AAAA
|
||||
// query, then the DNS64 SHOULD use the TTL of the original A RR or 600
|
||||
// seconds, whichever is shorter.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.1.7.
|
||||
maxDNS64SynTTL uint32 = 600
|
||||
)
|
||||
|
||||
// setupDNS64 initializes DNS64 settings, the NAT64 prefixes in particular. If
|
||||
@@ -38,227 +14,22 @@ const (
|
||||
// is specified explicitly. Each prefix also validated to be a valid IPv6
|
||||
// CIDR with a maximum length of 96 bits. The first specified prefix is then
|
||||
// used to synthesize AAAA records.
|
||||
func (s *Server) setupDNS64() (err error) {
|
||||
func (s *Server) setupDNS64() {
|
||||
if !s.conf.UseDNS64 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
l := len(s.conf.DNS64Prefixes)
|
||||
if l == 0 {
|
||||
s.dns64Prefs = []netip.Prefix{dns64WellKnownPref}
|
||||
if len(s.conf.DNS64Prefixes) == 0 {
|
||||
// dns64WellKnownPref is the default prefix to use in an algorithmic
|
||||
// mapping for DNS64.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc6052#section-2.1.
|
||||
dns64WellKnownPref := netip.MustParsePrefix("64:ff9b::/96")
|
||||
|
||||
return nil
|
||||
s.dns64Pref = dns64WellKnownPref
|
||||
} else {
|
||||
s.dns64Pref = s.conf.DNS64Prefixes[0]
|
||||
}
|
||||
|
||||
prefs := make([]netip.Prefix, 0, l)
|
||||
for i, pref := range s.conf.DNS64Prefixes {
|
||||
var p netip.Prefix
|
||||
p, err = netip.ParsePrefix(pref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prefix at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
addr := p.Addr()
|
||||
if !addr.Is6() {
|
||||
return fmt.Errorf("prefix at index %d: %q is not an IPv6 prefix", i, pref)
|
||||
}
|
||||
|
||||
if p.Bits() > maxNAT64PrefixBitLen {
|
||||
return fmt.Errorf("prefix at index %d: %q is too long for DNS64", i, pref)
|
||||
}
|
||||
|
||||
prefs = append(prefs, p.Masked())
|
||||
}
|
||||
|
||||
s.dns64Prefs = prefs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkDNS64 checks if DNS64 should be performed. It returns a DNS64 request
|
||||
// to resolve or nil if DNS64 is not desired. It also filters resp to not
|
||||
// contain any NAT64 excluded addresses in the answer section, if needed. Both
|
||||
// req and resp must not be nil.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc6147.
|
||||
func (s *Server) checkDNS64(req, resp *dns.Msg) (dns64Req *dns.Msg) {
|
||||
if len(s.dns64Prefs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := req.Question[0]
|
||||
if q.Qtype != dns.TypeAAAA || q.Qclass != dns.ClassINET {
|
||||
// DNS64 operation for classes other than IN is undefined, and a DNS64
|
||||
// MUST behave as though no DNS64 function is configured.
|
||||
return nil
|
||||
}
|
||||
|
||||
rcode := resp.Rcode
|
||||
if rcode == dns.RcodeNameError {
|
||||
// A result with RCODE=3 (Name Error) is handled according to normal DNS
|
||||
// operation (which is normally to return the error to the client).
|
||||
return nil
|
||||
}
|
||||
|
||||
if rcode == dns.RcodeSuccess {
|
||||
// If resolver receives an answer with at least one AAAA record
|
||||
// containing an address outside any of the excluded range(s), then it
|
||||
// by default SHOULD build an answer section for a response including
|
||||
// only the AAAA record(s) that do not contain any of the addresses
|
||||
// inside the excluded ranges.
|
||||
var hasAnswers bool
|
||||
if resp.Answer, hasAnswers = s.filterNAT64Answers(resp.Answer); hasAnswers {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Any other RCODE is treated as though the RCODE were 0 and the answer
|
||||
// section were empty.
|
||||
}
|
||||
|
||||
return &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: dns.Id(),
|
||||
RecursionDesired: req.RecursionDesired,
|
||||
AuthenticatedData: req.AuthenticatedData,
|
||||
CheckingDisabled: req.CheckingDisabled,
|
||||
},
|
||||
Question: []dns.Question{{
|
||||
Name: req.Question[0].Name,
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// filterNAT64Answers filters out AAAA records that are within one of NAT64
|
||||
// exclusion prefixes. hasAnswers is true if the filtered slice contains at
|
||||
// least a single AAAA answer not within the prefixes or a CNAME.
|
||||
func (s *Server) filterNAT64Answers(rrs []dns.RR) (filtered []dns.RR, hasAnswers bool) {
|
||||
filtered = make([]dns.RR, 0, len(rrs))
|
||||
for _, ans := range rrs {
|
||||
switch ans := ans.(type) {
|
||||
case *dns.AAAA:
|
||||
addr, err := netutil.IPToAddrNoMapped(ans.AAAA)
|
||||
if err != nil {
|
||||
log.Error("dnsforward: bad AAAA record: %s", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if s.withinDNS64(addr) {
|
||||
// Filter the record.
|
||||
continue
|
||||
}
|
||||
|
||||
filtered, hasAnswers = append(filtered, ans), true
|
||||
case *dns.CNAME, *dns.DNAME:
|
||||
// If the response contains a CNAME or a DNAME, then the CNAME or
|
||||
// DNAME chain is followed until the first terminating A or AAAA
|
||||
// record is reached.
|
||||
//
|
||||
// Just treat CNAME and DNAME responses as passable answers since
|
||||
// AdGuard Home doesn't follow any of these chains except the
|
||||
// dnsrewrite-defined ones.
|
||||
filtered, hasAnswers = append(filtered, ans), true
|
||||
default:
|
||||
filtered = append(filtered, ans)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, hasAnswers
|
||||
}
|
||||
|
||||
// synthDNS64 synthesizes a DNS64 response using the original response as a
|
||||
// basis and modifying it with data from resp. It returns true if the response
|
||||
// was actually modified.
|
||||
func (s *Server) synthDNS64(origReq, origResp, resp *dns.Msg) (ok bool) {
|
||||
if len(resp.Answer) == 0 {
|
||||
// If there is an empty answer, then the DNS64 responds to the original
|
||||
// querying client with the answer the DNS64 received to the original
|
||||
// (initiator's) query.
|
||||
return false
|
||||
}
|
||||
|
||||
// The Time to Live (TTL) field is set to the minimum of the TTL of the
|
||||
// original A RR and the SOA RR for the queried domain. If the original
|
||||
// response contains no SOA records, the minimum of the TTL of the original
|
||||
// A RR and [maxDNS64SynTTL] should be used. See [maxDNS64SynTTL].
|
||||
soaTTL := maxDNS64SynTTL
|
||||
for _, rr := range origResp.Ns {
|
||||
if hdr := rr.Header(); hdr.Rrtype == dns.TypeSOA && hdr.Name == origReq.Question[0].Name {
|
||||
soaTTL = hdr.Ttl
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newAns := make([]dns.RR, 0, len(resp.Answer))
|
||||
for _, ans := range resp.Answer {
|
||||
rr := s.synthRR(ans, soaTTL)
|
||||
if rr == nil {
|
||||
// The error should have already been logged.
|
||||
return false
|
||||
}
|
||||
|
||||
newAns = append(newAns, rr)
|
||||
}
|
||||
|
||||
origResp.Answer = newAns
|
||||
origResp.Ns = resp.Ns
|
||||
origResp.Extra = resp.Extra
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// dns64WellKnownPref is the default prefix to use in an algorithmic mapping for
|
||||
// DNS64. See https://datatracker.ietf.org/doc/html/rfc6052#section-2.1.
|
||||
var dns64WellKnownPref = netip.MustParsePrefix("64:ff9b::/96")
|
||||
|
||||
// withinDNS64 checks if ip is within one of the configured DNS64 prefixes.
|
||||
//
|
||||
// TODO(e.burkov): We actually using bytes of only the first prefix from the
|
||||
// set to construct the answer, so consider using some implementation of a
|
||||
// prefix set for the rest.
|
||||
func (s *Server) withinDNS64(ip netip.Addr) (ok bool) {
|
||||
for _, n := range s.dns64Prefs {
|
||||
if n.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldStripDNS64 returns true if DNS64 is enabled and ip has either one of
|
||||
// custom DNS64 prefixes or the Well-Known one. This is intended to be used
|
||||
// with PTR requests.
|
||||
//
|
||||
// The requirement is to match any Pref64::/n used at the site, and not merely
|
||||
// the locally configured Pref64::/n. This is because end clients could ask for
|
||||
// a PTR record matching an address received through a different (site-provided)
|
||||
// DNS64.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.3.1.
|
||||
func (s *Server) shouldStripDNS64(ip net.IP) (ok bool) {
|
||||
if len(s.dns64Prefs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
addr, err := netutil.IPToAddr(ip, netutil.AddrFamilyIPv6)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch {
|
||||
case s.withinDNS64(addr):
|
||||
log.Debug("dnsforward: %s is within DNS64 custom prefix set", ip)
|
||||
case dns64WellKnownPref.Contains(addr):
|
||||
log.Debug("dnsforward: %s is within DNS64 well-known prefix", ip)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// mapDNS64 maps ip to IPv6 address using configured DNS64 prefix. ip must be a
|
||||
@@ -267,79 +38,12 @@ func (s *Server) shouldStripDNS64(ip net.IP) (ok bool) {
|
||||
func (s *Server) mapDNS64(ip netip.Addr) (mapped net.IP) {
|
||||
// Don't mask the address here since it should have already been masked on
|
||||
// initialization stage.
|
||||
pref := s.dns64Prefs[0].Addr().As16()
|
||||
pref := s.dns64Pref.Masked().Addr().As16()
|
||||
ipData := ip.As4()
|
||||
|
||||
mapped = make(net.IP, net.IPv6len)
|
||||
copy(mapped[:nat64PrefixLen], pref[:])
|
||||
copy(mapped[nat64PrefixLen:], ipData[:])
|
||||
copy(mapped[:proxy.NAT64PrefixLength], pref[:])
|
||||
copy(mapped[proxy.NAT64PrefixLength:], ipData[:])
|
||||
|
||||
return mapped
|
||||
}
|
||||
|
||||
// performDNS64 processes the current state of dctx assuming that it has already
|
||||
// been tried to resolve, checks if it contains any acceptable response, and if
|
||||
// it doesn't, performs DNS64 request and the following synthesis. It returns
|
||||
// the [resultCodeError] if there was an error set to dctx.
|
||||
func (s *Server) performDNS64(prx *proxy.Proxy, dctx *dnsContext) (rc resultCode) {
|
||||
pctx := dctx.proxyCtx
|
||||
req := pctx.Req
|
||||
|
||||
dns64Req := s.checkDNS64(req, pctx.Res)
|
||||
if dns64Req == nil {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: received an empty AAAA response, checking DNS64")
|
||||
|
||||
origReq := pctx.Req
|
||||
origResp := pctx.Res
|
||||
origUps := pctx.Upstream
|
||||
|
||||
pctx.Req = dns64Req
|
||||
defer func() { pctx.Req = origReq }()
|
||||
|
||||
if dctx.err = prx.Resolve(pctx); dctx.err != nil {
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
dns64Resp := pctx.Res
|
||||
pctx.Res = origResp
|
||||
if dns64Resp != nil && s.synthDNS64(origReq, pctx.Res, dns64Resp) {
|
||||
log.Debug("dnsforward: synthesized AAAA response for %q", origReq.Question[0].Name)
|
||||
} else {
|
||||
pctx.Upstream = origUps
|
||||
}
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// synthRR synthesizes a DNS64 resource record in compliance with RFC 6147. If
|
||||
// rr is not an A record, it's returned as is. A records are modified to become
|
||||
// a DNS64-synthesized AAAA records, and the TTL is set according to the
|
||||
// original TTL of a record and soaTTL. It returns nil on invalid A records.
|
||||
func (s *Server) synthRR(rr dns.RR, soaTTL uint32) (result dns.RR) {
|
||||
aResp, ok := rr.(*dns.A)
|
||||
if !ok {
|
||||
return rr
|
||||
}
|
||||
|
||||
addr, err := netutil.IPToAddr(aResp.A, netutil.AddrFamilyIPv4)
|
||||
if err != nil {
|
||||
log.Error("dnsforward: bad A record: %s", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
aaaa := &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: aResp.Hdr.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: aResp.Hdr.Class,
|
||||
Ttl: mathutil.Min(aResp.Hdr.Ttl, soaTTL),
|
||||
},
|
||||
AAAA: s.mapDNS64(addr),
|
||||
}
|
||||
|
||||
return aaaa
|
||||
}
|
||||
|
||||
@@ -15,6 +15,16 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// maxDNS64SynTTL is the maximum TTL for synthesized DNS64 responses with no SOA
|
||||
// records in seconds.
|
||||
//
|
||||
// If the SOA RR was not delivered with the negative response to the AAAA query,
|
||||
// then the DNS64 SHOULD use the TTL of the original A RR or 600 seconds,
|
||||
// whichever is shorter.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.1.7.
|
||||
const maxDNS64SynTTL uint32 = 600
|
||||
|
||||
// newRR is a helper that creates a new dns.RR with the given name, qtype, ttl
|
||||
// and value. It fails the test if the qtype is not supported or the type of
|
||||
// value doesn't match the qtype.
|
||||
|
||||
@@ -80,10 +80,17 @@ type Server struct {
|
||||
privateNets netutil.SubnetSet
|
||||
localResolvers *proxy.Proxy
|
||||
sysResolvers aghnet.SystemResolvers
|
||||
recDetector *recursionDetector
|
||||
|
||||
// dns64Prefix is the set of NAT64 prefixes used for DNS64 handling.
|
||||
dns64Prefs []netip.Prefix
|
||||
// recDetector is a cache for recursive requests. It is used to detect
|
||||
// and prevent recursive requests only for private upstreams.
|
||||
//
|
||||
// See https://github.com/adguardTeam/adGuardHome/issues/3185#issuecomment-851048135.
|
||||
recDetector *recursionDetector
|
||||
|
||||
// dns64Pref is the NAT64 prefix used for DNS64 response mapping. The major
|
||||
// part of DNS64 happens inside the [proxy] package, but there still are
|
||||
// some places where response mapping is needed (e.g. DHCP).
|
||||
dns64Pref netip.Prefix
|
||||
|
||||
// anonymizer masks the client's IP addresses if needed.
|
||||
anonymizer *aghnet.IPMut
|
||||
@@ -246,8 +253,8 @@ func (s *Server) Resolve(host string) ([]net.IPAddr, error) {
|
||||
|
||||
// RDNSExchanger is a resolver for clients' addresses.
|
||||
type RDNSExchanger interface {
|
||||
// Exchange tries to resolve the ip in a suitable way, e.g. either as
|
||||
// local or as external.
|
||||
// Exchange tries to resolve the ip in a suitable way, i.e. either as local
|
||||
// or as external.
|
||||
Exchange(ip net.IP) (host string, err error)
|
||||
|
||||
// ResolvesPrivatePTR returns true if the RDNSExchanger is able to
|
||||
@@ -256,13 +263,13 @@ type RDNSExchanger interface {
|
||||
}
|
||||
|
||||
const (
|
||||
// rDNSEmptyAnswerErr is returned by Exchange method when the answer
|
||||
// section of respond is empty.
|
||||
rDNSEmptyAnswerErr errors.Error = "the answer section is empty"
|
||||
// ErrRDNSNoData is returned by [RDNSExchanger.Exchange] when the answer
|
||||
// section of response is either NODATA or has no PTR records.
|
||||
ErrRDNSNoData errors.Error = "no ptr data in response"
|
||||
|
||||
// rDNSNotPTRErr is returned by Exchange method when the response is not
|
||||
// of PTR type.
|
||||
rDNSNotPTRErr errors.Error = "the response is not a ptr"
|
||||
// ErrRDNSFailed is returned by [RDNSExchanger.Exchange] if the received
|
||||
// response is not a NOERROR or NXDOMAIN.
|
||||
ErrRDNSFailed errors.Error = "failed to resolve ptr"
|
||||
)
|
||||
|
||||
// type check
|
||||
@@ -317,17 +324,24 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Distinguish between NODATA response and a failed request.
|
||||
resp := ctx.Res
|
||||
if len(resp.Answer) == 0 {
|
||||
return "", fmt.Errorf("lookup for %q: %w", arpa, rDNSEmptyAnswerErr)
|
||||
if resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeNameError {
|
||||
return "", fmt.Errorf(
|
||||
"received %s response: %w",
|
||||
dns.RcodeToString[resp.Rcode],
|
||||
ErrRDNSFailed,
|
||||
)
|
||||
}
|
||||
|
||||
ptr, ok := resp.Answer[0].(*dns.PTR)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("type checking: %w", rDNSNotPTRErr)
|
||||
for _, ans := range resp.Answer {
|
||||
ptr, ok := ans.(*dns.PTR)
|
||||
if ok {
|
||||
return strings.TrimSuffix(ptr.Ptr, "."), nil
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(ptr.Ptr, "."), nil
|
||||
return "", ErrRDNSNoData
|
||||
}
|
||||
|
||||
// ResolvesPrivatePTR implements the RDNSExchanger interface for *Server.
|
||||
@@ -477,6 +491,8 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
|
||||
return fmt.Errorf("preparing proxy: %w", err)
|
||||
}
|
||||
|
||||
s.setupDNS64()
|
||||
|
||||
err = s.prepareInternalProxy()
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing internal proxy: %w", err)
|
||||
@@ -493,18 +509,18 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
|
||||
|
||||
s.registerHandlers()
|
||||
|
||||
err = s.setupDNS64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing DNS64: %w", err)
|
||||
}
|
||||
|
||||
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
||||
|
||||
// TODO(e.burkov): Remove once the local resolvers logic moved to dnsproxy.
|
||||
err = s.setupResolvers(s.conf.LocalPTRResolvers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up resolvers: %w", err)
|
||||
}
|
||||
|
||||
if s.conf.UsePrivateRDNS {
|
||||
proxyConfig.PrivateRDNSUpstreamConfig = s.localResolvers.UpstreamConfig
|
||||
}
|
||||
|
||||
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
||||
|
||||
s.recDetector.clear()
|
||||
|
||||
return nil
|
||||
|
||||
@@ -89,9 +89,14 @@ func createTestServer(
|
||||
s.serverLock.Lock()
|
||||
defer s.serverLock.Unlock()
|
||||
|
||||
// TODO(e.burkov): Try to move it higher.
|
||||
if localUps != nil {
|
||||
s.localResolvers.UpstreamConfig.Upstreams = []upstream.Upstream{localUps}
|
||||
ups := []upstream.Upstream{localUps}
|
||||
s.localResolvers.UpstreamConfig.Upstreams = ups
|
||||
s.conf.UsePrivateRDNS = true
|
||||
s.dnsProxy.PrivateRDNSUpstreamConfig = &proxy.UpstreamConfig{
|
||||
Upstreams: ups,
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -1216,6 +1221,9 @@ func TestServer_Exchange(t *testing.T) {
|
||||
|
||||
errUpstream := aghtest.NewErrorUpstream()
|
||||
nonPtrUpstream := aghtest.NewBlockUpstream("some-host", true)
|
||||
refusingUpstream := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
return new(dns.Msg).SetRcode(req, dns.RcodeRefused), nil
|
||||
})
|
||||
|
||||
srv := &Server{
|
||||
recDetector: newRecursionDetector(0, 1),
|
||||
@@ -1260,15 +1268,21 @@ func TestServer_Exchange(t *testing.T) {
|
||||
}, {
|
||||
name: "empty_answer_error",
|
||||
want: "",
|
||||
wantErr: rDNSEmptyAnswerErr,
|
||||
wantErr: ErrRDNSNoData,
|
||||
locUpstream: locUpstream,
|
||||
req: net.IP{192, 168, 1, 2},
|
||||
}, {
|
||||
name: "not_ptr_error",
|
||||
name: "invalid_answer",
|
||||
want: "",
|
||||
wantErr: rDNSNotPTRErr,
|
||||
wantErr: ErrRDNSNoData,
|
||||
locUpstream: nonPtrUpstream,
|
||||
req: localIP,
|
||||
}, {
|
||||
name: "refused",
|
||||
want: "",
|
||||
wantErr: ErrRDNSFailed,
|
||||
locUpstream: refusingUpstream,
|
||||
req: localIP,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -22,9 +22,11 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
|
||||
|
||||
shouldLog := true
|
||||
msg := pctx.Req
|
||||
q := msg.Question[0]
|
||||
host := strings.ToLower(strings.TrimSuffix(q.Name, "."))
|
||||
|
||||
// don't log ANY request if refuseAny is enabled
|
||||
if len(msg.Question) >= 1 && msg.Question[0].Qtype == dns.TypeANY && s.conf.RefuseAny {
|
||||
if q.Qtype == dns.TypeANY && s.conf.RefuseAny {
|
||||
shouldLog = false
|
||||
}
|
||||
|
||||
@@ -41,11 +43,20 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
|
||||
// Synchronize access to s.queryLog and s.stats so they won't be suddenly
|
||||
// uninitialized while in use. This can happen after proxy server has been
|
||||
// stopped, but its workers haven't yet exited.
|
||||
if shouldLog && s.queryLog != nil {
|
||||
if shouldLog &&
|
||||
s.queryLog != nil &&
|
||||
s.queryLog.ShouldLog(host, q.Qtype, q.Qclass) {
|
||||
s.logQuery(dctx, pctx, elapsed, ip)
|
||||
} else {
|
||||
log.Debug(
|
||||
"dnsforward: request %s %s from %s ignored; not logging",
|
||||
dns.Type(q.Qtype),
|
||||
host,
|
||||
ip,
|
||||
)
|
||||
}
|
||||
|
||||
if s.stats != nil {
|
||||
if s.stats != nil && s.stats.ShouldCount(host, q.Qtype, q.Qclass) {
|
||||
s.updateStats(dctx, elapsed, *dctx.result, ip)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,34 +16,44 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testQueryLog is a simple querylog.QueryLog implementation for tests.
|
||||
// testQueryLog is a simple [querylog.QueryLog] implementation for tests.
|
||||
type testQueryLog struct {
|
||||
// QueryLog is embedded here simply to make testQueryLog
|
||||
// a querylog.QueryLog without actually implementing all methods.
|
||||
// a [querylog.QueryLog] without actually implementing all methods.
|
||||
querylog.QueryLog
|
||||
|
||||
lastParams *querylog.AddParams
|
||||
}
|
||||
|
||||
// Add implements the querylog.QueryLog interface for *testQueryLog.
|
||||
// Add implements the [querylog.QueryLog] interface for *testQueryLog.
|
||||
func (l *testQueryLog) Add(p *querylog.AddParams) {
|
||||
l.lastParams = p
|
||||
}
|
||||
|
||||
// testStats is a simple stats.Stats implementation for tests.
|
||||
// ShouldLog implements the [querylog.QueryLog] interface for *testQueryLog.
|
||||
func (l *testQueryLog) ShouldLog(string, uint16, uint16) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// testStats is a simple [stats.Interface] implementation for tests.
|
||||
type testStats struct {
|
||||
// Stats is embedded here simply to make testStats a stats.Stats without
|
||||
// actually implementing all methods.
|
||||
// Stats is embedded here simply to make testStats a [stats.Interface]
|
||||
// without actually implementing all methods.
|
||||
stats.Interface
|
||||
|
||||
lastEntry stats.Entry
|
||||
}
|
||||
|
||||
// Update implements the stats.Stats interface for *testStats.
|
||||
// Update implements the [stats.Interface] interface for *testStats.
|
||||
func (l *testStats) Update(e stats.Entry) {
|
||||
l.lastEntry = e
|
||||
}
|
||||
|
||||
// ShouldCount implements the [stats.Interface] interface for *testStats.
|
||||
func (l *testStats) ShouldCount(string, uint16, uint16) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user