AdGuardHome/internal/dnsforward/dns64.go
Eugene Burkov 99ec1d840c Pull request: use mathutil
Merge in DNS/adguard-home from use-mathutil to master

Squashed commit of the following:

commit cfe2c02a7bb727e81fcd91674aa777df37413a8e
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Jan 24 18:12:32 2023 +0300

    all: use mathutil
2023-01-24 19:50:19 +03:00

345 lines
10 KiB
Go

package dnsforward
import (
"fmt"
"net"
"net/netip"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
)
const (
// maxNAT64PrefixBitLen is the maximum length of a NAT64 prefix in bits.
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.2.
maxNAT64PrefixBitLen = 96
// nat64PrefixLen is the length of a NAT64 prefix in bytes.
nat64PrefixLen = net.IPv6len - net.IPv4len
// maxDNS64SynTTL is the maximum TTL for synthesized DNS64 responses with no
// SOA records in seconds.
//
// If the SOA RR was not delivered with the negative response to the AAAA
// query, then the DNS64 SHOULD use the TTL of the original A RR or 600
// seconds, whichever is shorter.
//
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.1.7.
maxDNS64SynTTL uint32 = 600
)
// setupDNS64 initializes DNS64 settings, the NAT64 prefixes in particular. If
// 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,
Ttl: mathutil.Min(aResp.Hdr.Ttl, soaTTL),
},
AAAA: s.mapDNS64(addr),
}
return aaaa
}