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: de3e84b5 54a141ab
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: 42aa42a8 6e803375
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:
Eugene Burkov 2023-01-23 19:10:56 +03:00
parent 54a141abde
commit 2ecf2a4c42
9 changed files with 730 additions and 33 deletions

View File

@ -23,14 +23,23 @@ See also the [v0.107.23 GitHub milestone][ms-v0.107.23].
NOTE: Add new changes BELOW THIS COMMENT. 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 ### Removed
* The “beta frontend” and the corresponding APIs. They never quite worked - The “beta frontend” and the corresponding APIs. They never quite worked
properly, and the future new version of AdGuard Home API will probably be properly, and the future new version of AdGuard Home API will probably be
different. different.
Correspondingly, the configuration parameter `beta_bind_port` has been Correspondingly, the configuration parameter `beta_bind_port` has been removed
removed as well. as well.
[#5117]: https://github.com/AdguardTeam/AdGuardHome/issues/5117

View File

@ -224,6 +224,9 @@ type ServerConfig struct {
// resolving PTR queries for local addresses. // resolving PTR queries for local addresses.
LocalPTRResolvers []string 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 signals if the RDNS should resolve clients' addresses.
ResolveClients bool ResolveClients bool
@ -231,6 +234,9 @@ type ServerConfig struct {
// locally-served networks should be resolved via private PTR resolvers. // locally-served networks should be resolved via private PTR resolvers.
UsePrivateRDNS bool 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 defines if HTTP/3 is be allowed for incoming requests.
ServeHTTP3 bool ServeHTTP3 bool

View File

@ -28,9 +28,10 @@ type dnsContext struct {
// response is modified by filters. // response is modified by filters.
origResp *dns.Msg origResp *dns.Msg
// unreversedReqIP stores an IP address obtained from PTR request if it // unreversedReqIP stores an IP address obtained from a PTR request if it
// parsed successfully and belongs to one of locally-served IP ranges as per // was parsed successfully and belongs to one of the locally served IP
// RFC 6303. // ranges. It is also filled with unmapped version of the address if it's
// within DNS64 prefixes.
unreversedReqIP net.IP unreversedReqIP net.IP
// err is the error returned from a processing function. // 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 shows if the response had the AD bit set.
responseAD bool 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. // network.
isLocalClient bool isLocalClient bool
} }
@ -133,8 +134,8 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, pctx *proxy.DNSContext) error
return nil return nil
} }
// processRecursion checks the incoming request and halts it's handling if s // processRecursion checks the incoming request and halts its handling by
// have tried to resolve it recently. // answering NXDOMAIN if s has tried to resolve it recently.
func (s *Server) processRecursion(dctx *dnsContext) (rc resultCode) { func (s *Server) processRecursion(dctx *dnsContext) (rc resultCode) {
pctx := dctx.proxyCtx pctx := dctx.proxyCtx
@ -349,8 +350,8 @@ func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
return resp return resp
} }
// processDetermineLocal determines if the client's IP address is from // processDetermineLocal determines if the client's IP address is from locally
// locally-served network and saves the result into the context. // served network and saves the result into the context.
func (s *Server) processDetermineLocal(dctx *dnsContext) (rc resultCode) { func (s *Server) processDetermineLocal(dctx *dnsContext) (rc resultCode) {
rc = resultCodeSuccess 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 // 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. // TODO(a.garipov): Adapt to AAAA as well.
func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) { 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) log.Debug("dnsforward: dhcp record for %q is %s", reqHost, ip)
resp := s.makeResponse(req) resp := s.makeResponse(req)
if q.Qtype == dns.TypeA { switch q.Qtype {
case dns.TypeA:
a := &dns.A{ a := &dns.A{
Hdr: s.hdr(req, dns.TypeA), Hdr: s.hdr(req, dns.TypeA),
A: ip.AsSlice(), A: ip.AsSlice(),
} }
resp.Answer = append(resp.Answer, a) 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 dctx.proxyCtx.Res = resp
return resultCodeSuccess return resultCodeSuccess
} }
// processRestrictLocal responds with NXDOMAIN to PTR requests for IP addresses // 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) { func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
pctx := dctx.proxyCtx pctx := dctx.proxyCtx
req := pctx.Req req := pctx.Req
@ -452,15 +468,24 @@ func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
return resultCodeSuccess return resultCodeSuccess
} }
// Restrict an access to local addresses for external clients. We also if s.shouldStripDNS64(ip) {
// assume that all the DHCP leases we give are locally-served or at least // Strip the prefix from the address to get the original IPv4.
// don't need to be accessible externally. ip = ip[nat64PrefixLen:]
if !s.privateNets.Contains(ip) {
log.Debug("dnsforward: addr %s is not from locally-served network", ip)
// 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 return resultCodeSuccess
} }
log.Debug("dnsforward: addr %s is from locally served network", ip)
if !dctx.isLocalClient { if !dctx.isLocalClient {
log.Debug("dnsforward: %q requests an internal ip", pctx.Addr) log.Debug("dnsforward: %q requests an internal ip", pctx.Addr)
pctx.Res = s.genNXDomain(req) pctx.Res = s.genNXDomain(req)
@ -473,7 +498,7 @@ func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
dctx.unreversedReqIP = ip dctx.unreversedReqIP = ip
// There is no need to filter request from external addresses since this // 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. // hostname so disable redundant filters.
dctx.setts.ParentalEnabled = false dctx.setts.ParentalEnabled = false
dctx.setts.SafeBrowsingEnabled = false dctx.setts.SafeBrowsingEnabled = false
@ -508,7 +533,7 @@ func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
return resultCodeSuccess 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) ipAddr, err := netutil.IPToAddrNoMapped(ip)
if err != nil { if err != nil {
log.Debug("dnsforward: bad reverse ip %v from dhcp: %s", ip, err) 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() s.serverLock.RLock()
defer s.serverLock.RUnlock() defer s.serverLock.RUnlock()
if !s.privateNets.Contains(ip) {
return resultCodeSuccess
}
if s.conf.UsePrivateRDNS { if s.conf.UsePrivateRDNS {
s.recDetector.add(*pctx.Req) s.recDetector.add(*pctx.Req)
if err := s.localResolvers.Resolve(pctx); err != nil { if err := s.localResolvers.Resolve(pctx); err != nil {
@ -636,9 +657,8 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
origReqAD := false origReqAD := false
if s.conf.EnableDNSSEC { if s.conf.EnableDNSSEC {
if req.AuthenticatedData { origReqAD = req.AuthenticatedData
origReqAD = true if !req.AuthenticatedData {
} else {
req.AuthenticatedData = true req.AuthenticatedData = true
} }
} }
@ -655,6 +675,10 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
return resultCodeError return resultCodeError
} }
if s.performDNS64(prx, dctx) == resultCodeError {
return resultCodeError
}
dctx.responseFromUpstream = true dctx.responseFromUpstream = true
dctx.responseAD = pctx.Res.AuthenticatedData dctx.responseAD = pctx.Res.AuthenticatedData

View File

@ -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
}

View File

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

View File

@ -82,6 +82,9 @@ type Server struct {
sysResolvers aghnet.SystemResolvers sysResolvers aghnet.SystemResolvers
recDetector *recursionDetector 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 masks the client's IP addresses if needed.
anonymizer *aghnet.IPMut anonymizer *aghnet.IPMut
@ -488,9 +491,11 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
return fmt.Errorf("preparing access: %w", err) 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} s.dnsProxy = &proxy.Proxy{Config: proxyConfig}

View File

@ -712,6 +712,10 @@ func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) registerHandlers() { 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.MethodGet, "/control/dns_info", s.handleGetConfig)
s.conf.HTTPRegister(http.MethodPost, "/control/dns_config", s.handleSetConfig) s.conf.HTTPRegister(http.MethodPost, "/control/dns_config", s.handleSetConfig)
s.conf.HTTPRegister(http.MethodPost, "/control/test_upstream_dns", s.handleTestUpstreamDNS) 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. // See also https://github.com/AdguardTeam/AdGuardHome/issues/2628.
s.conf.HTTPRegister("", "/dns-query", s.handleDoH) s.conf.HTTPRegister("", "/dns-query", s.handleDoH)
s.conf.HTTPRegister("", "/dns-query/", s.handleDoH) s.conf.HTTPRegister("", "/dns-query/", s.handleDoH)
webRegistered = true
} }

View File

@ -184,6 +184,12 @@ type dnsConfig struct {
// for PTR queries for locally-served networks. // for PTR queries for locally-served networks.
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"` 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. // 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 // TODO(a.garipov): Add to the UI when HTTP/3 support is no longer

View File

@ -242,6 +242,8 @@ func generateServerConfig(
ConfigModified: onConfigModified, ConfigModified: onConfigModified,
HTTPRegister: httpReg, HTTPRegister: httpReg,
OnDNSRequest: onDNSRequest, OnDNSRequest: onDNSRequest,
UseDNS64: config.DNS.UseDNS64,
DNS64Prefixes: config.DNS.DNS64Prefixes,
} }
if tlsConf.Enabled { if tlsConf.Enabled {