Pull request: 5117-dns64
Merge in DNS/adguard-home from 5117-dns64 to master Updates #5117. Squashed commit of the following: commit 757d689134b85bdac9a6f5e43249866ec09ab7e3 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Mon Jan 23 19:06:18 2023 +0300 all: imp fmt commit b7a73c68c0b40bd3bda520c045c8110975c1827a Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Mon Jan 23 17:49:21 2023 +0300 all: rm unused, imp code commit 548feb6bd27b9774a9453d0570d37cdf557d4c3a Merge: de3e84b554a141ab
Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Mon Jan 23 14:08:12 2023 +0300 Merge branch 'master' into 5117-dns64 commit de3e84b52b8dbff70df3ca0ac3315c3d33576334 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Mon Jan 23 12:04:48 2023 +0300 dnsforward: imp code commit a580e92119e3dbadc8b1a6572dbecc679f69db40 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Fri Jan 20 18:24:33 2023 +0400 dnsforward: try again commit 67b7a365194939fe15e4907a3dc2fee44b019d08 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Fri Jan 20 18:08:23 2023 +0400 dnsforward: fix test on linux commit ca83e4178a3383e326bf528d209d8766fb3c60d3 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Fri Jan 20 17:37:48 2023 +0400 dnsforward: imp naming commit c4e477c7a12af4966cbcd4e5f003a72966dc5d61 Merge: 42aa42a86e803375
Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Fri Jan 20 17:30:03 2023 +0400 Merge branch 'master' into 5117-dns64 commit 42aa42a8149b6bb42eb0da6e88ede4b5065bbf2f Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Fri Jan 20 17:26:54 2023 +0400 dnsforward: imp test commit 4e91c675703f1453456ef9eea08157009ce6237a Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Wed Jan 18 12:32:55 2023 +0400 dnsforward: imp code, docs, add test commit 766ef757f61e7a555b8151b4783fa7aba5f566f7 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Jan 17 16:36:35 2023 +0400 dnsforward: imp docs commit 6825f372389988597d1879cf66342c410f3cfd47 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Jan 17 14:33:33 2023 +0400 internal: imp code, docs commit 1215316a338496b5bea2b20d697c7451bfbcc84b Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Fri Jan 13 21:24:50 2023 +0400 all: add dns64 support
This commit is contained in:
parent
54a141abde
commit
2ecf2a4c42
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -23,14 +23,23 @@ See also the [v0.107.23 GitHub milestone][ms-v0.107.23].
|
|||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
### Added
|
||||
|
||||
- DNS64 support ([#5117]). The function may be enabled with new `use_dns64`
|
||||
field under `dns` object in the configuration along with `dns64_prefixes`, the
|
||||
set of exclusion prefixes to filter AAAA responses. The Well-Known Prefix
|
||||
(`64:ff9b::/96`) is used if no custom prefixes are specified.
|
||||
|
||||
### Removed
|
||||
|
||||
* The “beta frontend” and the corresponding APIs. They never quite worked
|
||||
properly, and the future new version of AdGuard Home API will probably be
|
||||
different.
|
||||
- The “beta frontend” and the corresponding APIs. They never quite worked
|
||||
properly, and the future new version of AdGuard Home API will probably be
|
||||
different.
|
||||
|
||||
Correspondingly, the configuration parameter `beta_bind_port` has been
|
||||
removed as well.
|
||||
Correspondingly, the configuration parameter `beta_bind_port` has been removed
|
||||
as well.
|
||||
|
||||
[#5117]: https://github.com/AdguardTeam/AdGuardHome/issues/5117
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -224,6 +224,9 @@ type ServerConfig struct {
|
|||
// resolving PTR queries for local addresses.
|
||||
LocalPTRResolvers []string
|
||||
|
||||
// DNS64Prefixes is a slice of NAT64 prefixes to be used for DNS64.
|
||||
DNS64Prefixes []string
|
||||
|
||||
// ResolveClients signals if the RDNS should resolve clients' addresses.
|
||||
ResolveClients bool
|
||||
|
||||
|
@ -231,6 +234,9 @@ type ServerConfig struct {
|
|||
// locally-served networks should be resolved via private PTR resolvers.
|
||||
UsePrivateRDNS bool
|
||||
|
||||
// UseDNS64 defines if DNS64 is enabled for incoming requests.
|
||||
UseDNS64 bool
|
||||
|
||||
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
|
||||
ServeHTTP3 bool
|
||||
|
||||
|
|
|
@ -28,9 +28,10 @@ type dnsContext struct {
|
|||
// response is modified by filters.
|
||||
origResp *dns.Msg
|
||||
|
||||
// unreversedReqIP stores an IP address obtained from PTR request if it
|
||||
// parsed successfully and belongs to one of locally-served IP ranges as per
|
||||
// RFC 6303.
|
||||
// unreversedReqIP stores an IP address obtained from a PTR request if it
|
||||
// was parsed successfully and belongs to one of the locally served IP
|
||||
// ranges. It is also filled with unmapped version of the address if it's
|
||||
// within DNS64 prefixes.
|
||||
unreversedReqIP net.IP
|
||||
|
||||
// err is the error returned from a processing function.
|
||||
|
@ -57,7 +58,7 @@ type dnsContext struct {
|
|||
// responseAD shows if the response had the AD bit set.
|
||||
responseAD bool
|
||||
|
||||
// isLocalClient shows if client's IP address is from locally-served
|
||||
// isLocalClient shows if client's IP address is from locally served
|
||||
// network.
|
||||
isLocalClient bool
|
||||
}
|
||||
|
@ -133,8 +134,8 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, pctx *proxy.DNSContext) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// processRecursion checks the incoming request and halts it's handling if s
|
||||
// have tried to resolve it recently.
|
||||
// processRecursion checks the incoming request and halts its handling by
|
||||
// answering NXDOMAIN if s has tried to resolve it recently.
|
||||
func (s *Server) processRecursion(dctx *dnsContext) (rc resultCode) {
|
||||
pctx := dctx.proxyCtx
|
||||
|
||||
|
@ -349,8 +350,8 @@ func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
|
|||
return resp
|
||||
}
|
||||
|
||||
// processDetermineLocal determines if the client's IP address is from
|
||||
// locally-served network and saves the result into the context.
|
||||
// processDetermineLocal determines if the client's IP address is from locally
|
||||
// served network and saves the result into the context.
|
||||
func (s *Server) processDetermineLocal(dctx *dnsContext) (rc resultCode) {
|
||||
rc = resultCodeSuccess
|
||||
|
||||
|
@ -377,7 +378,8 @@ func (s *Server) dhcpHostToIP(host string) (ip netip.Addr, ok bool) {
|
|||
}
|
||||
|
||||
// processDHCPHosts respond to A requests if the target hostname is known to
|
||||
// the server.
|
||||
// the server. It responds with a mapped IP address if the DNS64 is enabled and
|
||||
// the request is for AAAA.
|
||||
//
|
||||
// TODO(a.garipov): Adapt to AAAA as well.
|
||||
func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
|
||||
|
@ -409,20 +411,34 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
|
|||
log.Debug("dnsforward: dhcp record for %q is %s", reqHost, ip)
|
||||
|
||||
resp := s.makeResponse(req)
|
||||
if q.Qtype == dns.TypeA {
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
a := &dns.A{
|
||||
Hdr: s.hdr(req, dns.TypeA),
|
||||
A: ip.AsSlice(),
|
||||
}
|
||||
resp.Answer = append(resp.Answer, a)
|
||||
case dns.TypeAAAA:
|
||||
if len(s.dns64Prefs) > 0 {
|
||||
// Respond with DNS64-mapped address for IPv4 host if DNS64 is
|
||||
// enabled.
|
||||
aaaa := &dns.AAAA{
|
||||
Hdr: s.hdr(req, dns.TypeAAAA),
|
||||
AAAA: s.mapDNS64(ip),
|
||||
}
|
||||
resp.Answer = append(resp.Answer, aaaa)
|
||||
}
|
||||
default:
|
||||
// Go on.
|
||||
}
|
||||
|
||||
dctx.proxyCtx.Res = resp
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// processRestrictLocal responds with NXDOMAIN to PTR requests for IP addresses
|
||||
// in locally-served network from external clients.
|
||||
// in locally served network from external clients.
|
||||
func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
|
||||
pctx := dctx.proxyCtx
|
||||
req := pctx.Req
|
||||
|
@ -452,15 +468,24 @@ func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
|
|||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// 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
|
||||
// don't need to be accessible externally.
|
||||
if !s.privateNets.Contains(ip) {
|
||||
log.Debug("dnsforward: addr %s is not from locally-served network", ip)
|
||||
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.
|
||||
if !s.privateNets.Contains(ip) {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: addr %s is from locally served network", ip)
|
||||
|
||||
if !dctx.isLocalClient {
|
||||
log.Debug("dnsforward: %q requests an internal ip", pctx.Addr)
|
||||
pctx.Res = s.genNXDomain(req)
|
||||
|
@ -473,7 +498,7 @@ func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
|
|||
dctx.unreversedReqIP = ip
|
||||
|
||||
// There is no need to filter request from external addresses since this
|
||||
// code is only executed when the request is for locally-served ARPA
|
||||
// code is only executed when the request is for locally served ARPA
|
||||
// hostname so disable redundant filters.
|
||||
dctx.setts.ParentalEnabled = false
|
||||
dctx.setts.SafeBrowsingEnabled = false
|
||||
|
@ -508,7 +533,7 @@ func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
|
|||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Remove once we switch to netip.Addr more fully.
|
||||
// TODO(a.garipov): Remove once we switch to [netip.Addr] more fully.
|
||||
ipAddr, err := netutil.IPToAddrNoMapped(ip)
|
||||
if err != nil {
|
||||
log.Debug("dnsforward: bad reverse ip %v from dhcp: %s", ip, err)
|
||||
|
@ -556,10 +581,6 @@ func (s *Server) processLocalPTR(dctx *dnsContext) (rc resultCode) {
|
|||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
if !s.privateNets.Contains(ip) {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
if s.conf.UsePrivateRDNS {
|
||||
s.recDetector.add(*pctx.Req)
|
||||
if err := s.localResolvers.Resolve(pctx); err != nil {
|
||||
|
@ -636,9 +657,8 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
|
|||
|
||||
origReqAD := false
|
||||
if s.conf.EnableDNSSEC {
|
||||
if req.AuthenticatedData {
|
||||
origReqAD = true
|
||||
} else {
|
||||
origReqAD = req.AuthenticatedData
|
||||
if !req.AuthenticatedData {
|
||||
req.AuthenticatedData = true
|
||||
}
|
||||
}
|
||||
|
@ -655,6 +675,10 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
|
|||
return resultCodeError
|
||||
}
|
||||
|
||||
if s.performDNS64(prx, dctx) == resultCodeError {
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
dctx.responseFromUpstream = true
|
||||
dctx.responseAD = pctx.Res.AuthenticatedData
|
||||
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
package dnsforward
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"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
|
||||
// the DNS64 feature is enabled and no prefixes are configured, the default
|
||||
// Well-Known Prefix is used, just like Section 5.2 of RFC 6147 prescribes. Any
|
||||
// configured set of prefixes discards the default Well-Known prefix unless it
|
||||
// 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) {
|
||||
if !s.conf.UseDNS64 {
|
||||
return nil
|
||||
}
|
||||
|
||||
l := len(s.conf.DNS64Prefixes)
|
||||
if l == 0 {
|
||||
s.dns64Prefs = []netip.Prefix{dns64WellKnownPref}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
// valid IPv4. It panics, if there are no configured DNS64 prefixes, because
|
||||
// synthesis should not be performed unless DNS64 function enabled.
|
||||
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()
|
||||
ipData := ip.As4()
|
||||
|
||||
mapped = make(net.IP, net.IPv6len)
|
||||
copy(mapped[:nat64PrefixLen], pref[:])
|
||||
copy(mapped[nat64PrefixLen:], 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,
|
||||
},
|
||||
AAAA: s.mapDNS64(addr),
|
||||
}
|
||||
|
||||
if rrTTL := aResp.Hdr.Ttl; rrTTL < soaTTL {
|
||||
aaaa.Hdr.Ttl = rrTTL
|
||||
} else {
|
||||
aaaa.Hdr.Ttl = soaTTL
|
||||
}
|
||||
|
||||
return aaaa
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
package dnsforward
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func newRR(t *testing.T, name string, qtype uint16, ttl uint32, val any) (rr dns.RR) {
|
||||
t.Helper()
|
||||
|
||||
switch qtype {
|
||||
case dns.TypeA:
|
||||
rr = &dns.A{A: testutil.RequireTypeAssert[net.IP](t, val)}
|
||||
case dns.TypeAAAA:
|
||||
rr = &dns.AAAA{AAAA: testutil.RequireTypeAssert[net.IP](t, val)}
|
||||
case dns.TypeCNAME:
|
||||
rr = &dns.CNAME{Target: testutil.RequireTypeAssert[string](t, val)}
|
||||
case dns.TypeSOA:
|
||||
rr = &dns.SOA{
|
||||
Ns: "ns." + name,
|
||||
Mbox: "hostmaster." + name,
|
||||
Serial: 1,
|
||||
Refresh: 1,
|
||||
Retry: 1,
|
||||
Expire: 1,
|
||||
Minttl: 1,
|
||||
}
|
||||
case dns.TypePTR:
|
||||
rr = &dns.PTR{Ptr: testutil.RequireTypeAssert[string](t, val)}
|
||||
default:
|
||||
t.Fatalf("unsupported qtype: %d", qtype)
|
||||
}
|
||||
|
||||
*rr.Header() = dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: qtype,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
}
|
||||
|
||||
return rr
|
||||
}
|
||||
|
||||
func TestServer_HandleDNSRequest_dns64(t *testing.T) {
|
||||
const (
|
||||
ipv4Domain = "ipv4.only."
|
||||
ipv6Domain = "ipv6.only."
|
||||
soaDomain = "ipv4.soa."
|
||||
mappedDomain = "filterable.ipv6."
|
||||
anotherDomain = "another.domain."
|
||||
|
||||
pointedDomain = "local1234.ipv4."
|
||||
globDomain = "real1234.ipv4."
|
||||
)
|
||||
|
||||
someIPv4 := net.IP{1, 2, 3, 4}
|
||||
someIPv6 := net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
mappedIPv6 := net.ParseIP("64:ff9b::102:304")
|
||||
|
||||
ptr64Domain, err := netutil.IPToReversedAddr(mappedIPv6)
|
||||
require.NoError(t, err)
|
||||
ptr64Domain = dns.Fqdn(ptr64Domain)
|
||||
|
||||
ptrGlobDomain, err := netutil.IPToReversedAddr(someIPv4)
|
||||
require.NoError(t, err)
|
||||
ptrGlobDomain = dns.Fqdn(ptrGlobDomain)
|
||||
|
||||
const (
|
||||
sectionAnswer = iota
|
||||
sectionAuthority
|
||||
sectionAdditional
|
||||
|
||||
sectionsNum
|
||||
)
|
||||
|
||||
// answerMap is a convenience alias for describing the upstream response for
|
||||
// a given question type.
|
||||
type answerMap = map[uint16][sectionsNum][]dns.RR
|
||||
|
||||
pt := testutil.PanicT{}
|
||||
newUps := func(answers answerMap) (u upstream.Upstream) {
|
||||
return aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
q := req.Question[0]
|
||||
require.Contains(pt, answers, q.Qtype)
|
||||
|
||||
answer := answers[q.Qtype]
|
||||
|
||||
resp = (&dns.Msg{}).SetReply(req)
|
||||
resp.Answer = answer[sectionAnswer]
|
||||
resp.Ns = answer[sectionAuthority]
|
||||
resp.Extra = answer[sectionAdditional]
|
||||
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
qname string
|
||||
upsAns answerMap
|
||||
wantAns []dns.RR
|
||||
qtype uint16
|
||||
}{{
|
||||
name: "simple_a",
|
||||
qname: ipv4Domain,
|
||||
upsAns: answerMap{
|
||||
dns.TypeA: {
|
||||
sectionAnswer: {newRR(t, ipv4Domain, dns.TypeA, 3600, someIPv4)},
|
||||
},
|
||||
dns.TypeAAAA: {},
|
||||
},
|
||||
wantAns: []dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: ipv4Domain,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 3600,
|
||||
Rdlength: 4,
|
||||
},
|
||||
A: someIPv4,
|
||||
}},
|
||||
qtype: dns.TypeA,
|
||||
}, {
|
||||
name: "simple_aaaa",
|
||||
qname: ipv6Domain,
|
||||
upsAns: answerMap{
|
||||
dns.TypeA: {},
|
||||
dns.TypeAAAA: {
|
||||
sectionAnswer: {newRR(t, ipv6Domain, dns.TypeAAAA, 3600, someIPv6)},
|
||||
},
|
||||
},
|
||||
wantAns: []dns.RR{&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: ipv6Domain,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 3600,
|
||||
Rdlength: 16,
|
||||
},
|
||||
AAAA: someIPv6,
|
||||
}},
|
||||
qtype: dns.TypeAAAA,
|
||||
}, {
|
||||
name: "actual_dns64",
|
||||
qname: ipv4Domain,
|
||||
upsAns: answerMap{
|
||||
dns.TypeA: {
|
||||
sectionAnswer: {newRR(t, ipv4Domain, dns.TypeA, 3600, someIPv4)},
|
||||
},
|
||||
dns.TypeAAAA: {},
|
||||
},
|
||||
wantAns: []dns.RR{&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: ipv4Domain,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: maxDNS64SynTTL,
|
||||
Rdlength: 16,
|
||||
},
|
||||
AAAA: mappedIPv6,
|
||||
}},
|
||||
qtype: dns.TypeAAAA,
|
||||
}, {
|
||||
name: "actual_dns64_soattl",
|
||||
qname: soaDomain,
|
||||
upsAns: answerMap{
|
||||
dns.TypeA: {
|
||||
sectionAnswer: {newRR(t, soaDomain, dns.TypeA, 3600, someIPv4)},
|
||||
},
|
||||
dns.TypeAAAA: {
|
||||
sectionAuthority: {newRR(t, soaDomain, dns.TypeSOA, maxDNS64SynTTL+50, nil)},
|
||||
},
|
||||
},
|
||||
wantAns: []dns.RR{&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: soaDomain,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: maxDNS64SynTTL + 50,
|
||||
Rdlength: 16,
|
||||
},
|
||||
AAAA: mappedIPv6,
|
||||
}},
|
||||
qtype: dns.TypeAAAA,
|
||||
}, {
|
||||
name: "filtered",
|
||||
qname: mappedDomain,
|
||||
upsAns: answerMap{
|
||||
dns.TypeA: {},
|
||||
dns.TypeAAAA: {
|
||||
sectionAnswer: {
|
||||
newRR(t, mappedDomain, dns.TypeAAAA, 3600, net.ParseIP("64:ff9b::506:708")),
|
||||
newRR(t, mappedDomain, dns.TypeCNAME, 3600, anotherDomain),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAns: []dns.RR{&dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: mappedDomain,
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 3600,
|
||||
Rdlength: 16,
|
||||
},
|
||||
Target: anotherDomain,
|
||||
}},
|
||||
qtype: dns.TypeAAAA,
|
||||
}, {
|
||||
name: "ptr",
|
||||
qname: ptr64Domain,
|
||||
upsAns: nil,
|
||||
wantAns: []dns.RR{&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: ptr64Domain,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 3600,
|
||||
Rdlength: 16,
|
||||
},
|
||||
Ptr: pointedDomain,
|
||||
}},
|
||||
qtype: dns.TypePTR,
|
||||
}, {
|
||||
name: "ptr_glob",
|
||||
qname: ptrGlobDomain,
|
||||
upsAns: answerMap{
|
||||
dns.TypePTR: {
|
||||
sectionAnswer: {newRR(t, ptrGlobDomain, dns.TypePTR, 3600, globDomain)},
|
||||
},
|
||||
},
|
||||
wantAns: []dns.RR{&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: ptrGlobDomain,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 3600,
|
||||
Rdlength: 15,
|
||||
},
|
||||
Ptr: globDomain,
|
||||
}},
|
||||
qtype: dns.TypePTR,
|
||||
}}
|
||||
|
||||
localRR := newRR(t, ptr64Domain, dns.TypePTR, 3600, pointedDomain)
|
||||
localUps := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
require.Equal(pt, req.Question[0].Name, ptr64Domain)
|
||||
resp = (&dns.Msg{}).SetReply(req)
|
||||
resp.Answer = []dns.RR{localRR}
|
||||
|
||||
return resp, nil
|
||||
})
|
||||
|
||||
s := createTestServer(t, &filtering.Config{}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
UseDNS64: true,
|
||||
}, localUps)
|
||||
|
||||
client := &dns.Client{
|
||||
Net: "tcp",
|
||||
Timeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newUps(tc.upsAns)}
|
||||
startDeferStop(t, s)
|
||||
|
||||
req := (&dns.Msg{}).SetQuestion(tc.qname, tc.qtype)
|
||||
|
||||
resp, _, excErr := client.Exchange(req, s.dnsProxy.Addr(proxy.ProtoTCP).String())
|
||||
require.NoError(t, excErr)
|
||||
|
||||
require.Equal(t, tc.wantAns, resp.Answer)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -82,6 +82,9 @@ type Server struct {
|
|||
sysResolvers aghnet.SystemResolvers
|
||||
recDetector *recursionDetector
|
||||
|
||||
// dns64Prefix is the set of NAT64 prefixes used for DNS64 handling.
|
||||
dns64Prefs []netip.Prefix
|
||||
|
||||
// anonymizer masks the client's IP addresses if needed.
|
||||
anonymizer *aghnet.IPMut
|
||||
|
||||
|
@ -488,9 +491,11 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
|
|||
return fmt.Errorf("preparing access: %w", err)
|
||||
}
|
||||
|
||||
if !webRegistered && s.conf.HTTPRegister != nil {
|
||||
webRegistered = true
|
||||
s.registerHandlers()
|
||||
s.registerHandlers()
|
||||
|
||||
err = s.setupDNS64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing DNS64: %w", err)
|
||||
}
|
||||
|
||||
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
||||
|
|
|
@ -712,6 +712,10 @@ func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (s *Server) registerHandlers() {
|
||||
if webRegistered || s.conf.HTTPRegister == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.conf.HTTPRegister(http.MethodGet, "/control/dns_info", s.handleGetConfig)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dns_config", s.handleSetConfig)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/test_upstream_dns", s.handleTestUpstreamDNS)
|
||||
|
@ -730,4 +734,6 @@ func (s *Server) registerHandlers() {
|
|||
// See also https://github.com/AdguardTeam/AdGuardHome/issues/2628.
|
||||
s.conf.HTTPRegister("", "/dns-query", s.handleDoH)
|
||||
s.conf.HTTPRegister("", "/dns-query/", s.handleDoH)
|
||||
|
||||
webRegistered = true
|
||||
}
|
||||
|
|
|
@ -184,6 +184,12 @@ type dnsConfig struct {
|
|||
// for PTR queries for locally-served networks.
|
||||
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
|
||||
|
||||
// UseDNS64 defines if DNS64 should be used for incoming requests.
|
||||
UseDNS64 bool `yaml:"use_dns64"`
|
||||
|
||||
// DNS64Prefixes is the list of NAT64 prefixes to be used for DNS64.
|
||||
DNS64Prefixes []string `yaml:"dns64_prefixes"`
|
||||
|
||||
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
|
||||
//
|
||||
// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
|
||||
|
|
|
@ -242,6 +242,8 @@ func generateServerConfig(
|
|||
ConfigModified: onConfigModified,
|
||||
HTTPRegister: httpReg,
|
||||
OnDNSRequest: onDNSRequest,
|
||||
UseDNS64: config.DNS.UseDNS64,
|
||||
DNS64Prefixes: config.DNS.DNS64Prefixes,
|
||||
}
|
||||
|
||||
if tlsConf.Enabled {
|
||||
|
|
Loading…
Reference in New Issue