diff --git a/CHANGELOG.md b/CHANGELOG.md index add4b3e9..25949716 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 6357d681..c30d0d89 100644 --- a/internal/dnsforward/config.go +++ b/internal/dnsforward/config.go @@ -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 diff --git a/internal/dnsforward/dns.go b/internal/dnsforward/dns.go index e4ca6520..1411a0f4 100644 --- a/internal/dnsforward/dns.go +++ b/internal/dnsforward/dns.go @@ -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 diff --git a/internal/dnsforward/dns64.go b/internal/dnsforward/dns64.go new file mode 100644 index 00000000..28afb8cc --- /dev/null +++ b/internal/dnsforward/dns64.go @@ -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 +} diff --git a/internal/dnsforward/dns64_test.go b/internal/dnsforward/dns64_test.go new file mode 100644 index 00000000..12925504 --- /dev/null +++ b/internal/dnsforward/dns64_test.go @@ -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) + }) + } +} diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go index 8999845b..4ff9fc02 100644 --- a/internal/dnsforward/dnsforward.go +++ b/internal/dnsforward/dnsforward.go @@ -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} diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go index 5668573b..18c4e82e 100644 --- a/internal/dnsforward/http.go +++ b/internal/dnsforward/http.go @@ -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 } diff --git a/internal/home/config.go b/internal/home/config.go index d5b8fdf3..f2fc98f5 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -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 diff --git a/internal/home/dns.go b/internal/home/dns.go index 9d073d7b..4db9b1b2 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -242,6 +242,8 @@ func generateServerConfig( ConfigModified: onConfigModified, HTTPRegister: httpReg, OnDNSRequest: onDNSRequest, + UseDNS64: config.DNS.UseDNS64, + DNS64Prefixes: config.DNS.DNS64Prefixes, } if tlsConf.Enabled {