mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-22 21:15:35 +03:00
99ec1d840c
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
345 lines
10 KiB
Go
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
|
|
}
|