AdGuardHome/internal/dnsforward/msg.go
Eugene Burkov 184f476bdc Pull request 2269: ADG-8932 Upd all
Squashed commit of the following:

commit 00fc45877776ed7d1c59be26330f6f16d784ead2
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Aug 20 16:21:25 2024 +0300

    all: imp lint

commit b04d9cd334a92faf21787e7e1ebf20d5e5fd0bee
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Aug 20 14:40:18 2024 +0300

    all: upd all

commit f151f8c313
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Aug 16 13:12:36 2024 +0300

    all: upd proxy
2024-08-20 18:38:04 +03:00

431 lines
12 KiB
Go

package dnsforward
import (
"net/netip"
"slices"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
)
// TODO(e.burkov): Name all the methods by a [proxy.MessageConstructor]
// template. Also extract all the methods to a separate entity.
// reply creates a DNS response for req.
func (*Server) reply(req *dns.Msg, code int) (resp *dns.Msg) {
resp = (&dns.Msg{}).SetRcode(req, code)
resp.RecursionAvailable = true
return resp
}
// replyCompressed creates a DNS response for req and sets the compress flag.
func (s *Server) replyCompressed(req *dns.Msg) (resp *dns.Msg) {
resp = s.reply(req, dns.RcodeSuccess)
resp.Compress = true
return resp
}
// ipsFromRules extracts unique non-IP addresses from the filtering result
// rules.
func ipsFromRules(resRules []*filtering.ResultRule) (ips []netip.Addr) {
for _, r := range resRules {
// len(resRules) and len(ips) are actually small enough for O(n^2) to do
// not raise performance questions.
if ip := r.IP; ip != (netip.Addr{}) && !slices.Contains(ips, ip) {
ips = append(ips, ip)
}
}
return ips
}
// genDNSFilterMessage generates a filtered response to req for the filtering
// result res.
func (s *Server) genDNSFilterMessage(
dctx *proxy.DNSContext,
res *filtering.Result,
) (resp *dns.Msg) {
req := dctx.Req
qt := req.Question[0].Qtype
if qt != dns.TypeA && qt != dns.TypeAAAA && qt != dns.TypeHTTPS {
m, _, _ := s.dnsFilter.BlockingMode()
if m == filtering.BlockingModeNullIP {
return s.replyCompressed(req)
}
return s.NewMsgNODATA(req)
}
switch res.Reason {
case filtering.FilteredSafeBrowsing:
return s.genBlockedHost(req, s.dnsFilter.SafeBrowsingBlockHost(), dctx)
case filtering.FilteredParental:
return s.genBlockedHost(req, s.dnsFilter.ParentalBlockHost(), dctx)
case filtering.FilteredSafeSearch:
// If Safe Search generated the necessary IP addresses, use them.
// Otherwise, if there were no errors, there are no addresses for the
// requested IP version, so produce a NODATA response.
return s.getCNAMEWithIPs(req, ipsFromRules(res.Rules), res.CanonName)
default:
return s.genForBlockingMode(req, ipsFromRules(res.Rules))
}
}
// getCNAMEWithIPs generates a filtered response to req for with CNAME record
// and provided ips.
func (s *Server) getCNAMEWithIPs(req *dns.Msg, ips []netip.Addr, cname string) (resp *dns.Msg) {
resp = s.replyCompressed(req)
originalName := req.Question[0].Name
var ans []dns.RR
if cname != "" {
ans = append(ans, s.genAnswerCNAME(req, cname))
// The given IPs actually are resolved for this cname.
req.Question[0].Name = dns.Fqdn(cname)
defer func() { req.Question[0].Name = originalName }()
}
switch req.Question[0].Qtype {
case dns.TypeA:
ans = append(ans, s.genAnswersWithIPv4s(req, ips)...)
case dns.TypeAAAA:
for _, ip := range ips {
if ip.Is6() {
ans = append(ans, s.genAnswerAAAA(req, ip))
}
}
default:
// Go on and return an empty response.
}
resp.Answer = ans
return resp
}
// genForBlockingMode generates a filtered response to req based on the server's
// blocking mode.
func (s *Server) genForBlockingMode(req *dns.Msg, ips []netip.Addr) (resp *dns.Msg) {
switch mode, bIPv4, bIPv6 := s.dnsFilter.BlockingMode(); mode {
case filtering.BlockingModeCustomIP:
return s.makeResponseCustomIP(req, bIPv4, bIPv6)
case filtering.BlockingModeDefault:
if len(ips) > 0 {
return s.genResponseWithIPs(req, ips)
}
return s.makeResponseNullIP(req)
case filtering.BlockingModeNullIP:
return s.makeResponseNullIP(req)
case filtering.BlockingModeNXDOMAIN:
return s.NewMsgNXDOMAIN(req)
case filtering.BlockingModeREFUSED:
return s.makeResponseREFUSED(req)
default:
log.Error("dnsforward: invalid blocking mode %q", mode)
return s.replyCompressed(req)
}
}
// makeResponseCustomIP generates a DNS response message for Custom IP blocking
// mode with the provided IP addresses and an appropriate resource record type.
func (s *Server) makeResponseCustomIP(
req *dns.Msg,
bIPv4 netip.Addr,
bIPv6 netip.Addr,
) (resp *dns.Msg) {
switch qt := req.Question[0].Qtype; qt {
case dns.TypeA:
return s.genARecord(req, bIPv4)
case dns.TypeAAAA:
return s.genAAAARecord(req, bIPv6)
default:
// Generally shouldn't happen, since the types are checked in
// genDNSFilterMessage.
log.Error("dnsforward: invalid msg type %s for custom IP blocking mode", dns.Type(qt))
return s.replyCompressed(req)
}
}
func (s *Server) genARecord(request *dns.Msg, ip netip.Addr) *dns.Msg {
resp := s.replyCompressed(request)
resp.Answer = append(resp.Answer, s.genAnswerA(request, ip))
return resp
}
func (s *Server) genAAAARecord(request *dns.Msg, ip netip.Addr) *dns.Msg {
resp := s.replyCompressed(request)
resp.Answer = append(resp.Answer, s.genAnswerAAAA(request, ip))
return resp
}
func (s *Server) hdr(req *dns.Msg, rrType rules.RRType) (h dns.RR_Header) {
return dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: rrType,
Ttl: s.dnsFilter.BlockedResponseTTL(),
Class: dns.ClassINET,
}
}
func (s *Server) genAnswerA(req *dns.Msg, ip netip.Addr) (ans *dns.A) {
return &dns.A{
Hdr: s.hdr(req, dns.TypeA),
A: ip.AsSlice(),
}
}
func (s *Server) genAnswerAAAA(req *dns.Msg, ip netip.Addr) (ans *dns.AAAA) {
return &dns.AAAA{
Hdr: s.hdr(req, dns.TypeAAAA),
AAAA: ip.AsSlice(),
}
}
func (s *Server) genAnswerCNAME(req *dns.Msg, cname string) (ans *dns.CNAME) {
return &dns.CNAME{
Hdr: s.hdr(req, dns.TypeCNAME),
Target: dns.Fqdn(cname),
}
}
func (s *Server) genAnswerMX(req *dns.Msg, mx *rules.DNSMX) (ans *dns.MX) {
return &dns.MX{
Hdr: s.hdr(req, dns.TypeMX),
Preference: mx.Preference,
Mx: dns.Fqdn(mx.Exchange),
}
}
func (s *Server) genAnswerPTR(req *dns.Msg, ptr string) (ans *dns.PTR) {
return &dns.PTR{
Hdr: s.hdr(req, dns.TypePTR),
Ptr: dns.Fqdn(ptr),
}
}
func (s *Server) genAnswerSRV(req *dns.Msg, srv *rules.DNSSRV) (ans *dns.SRV) {
return &dns.SRV{
Hdr: s.hdr(req, dns.TypeSRV),
Priority: srv.Priority,
Weight: srv.Weight,
Port: srv.Port,
Target: dns.Fqdn(srv.Target),
}
}
func (s *Server) genAnswerTXT(req *dns.Msg, strs []string) (ans *dns.TXT) {
return &dns.TXT{
Hdr: s.hdr(req, dns.TypeTXT),
Txt: strs,
}
}
// genResponseWithIPs generates a DNS response message with the provided IP
// addresses and an appropriate resource record type. If any of the IPs cannot
// be converted to the correct protocol, genResponseWithIPs returns an empty
// response.
func (s *Server) genResponseWithIPs(req *dns.Msg, ips []netip.Addr) (resp *dns.Msg) {
var ans []dns.RR
switch req.Question[0].Qtype {
case dns.TypeA:
ans = s.genAnswersWithIPv4s(req, ips)
case dns.TypeAAAA:
for _, ip := range ips {
if ip.Is6() {
ans = append(ans, s.genAnswerAAAA(req, ip))
}
}
default:
// Go on and return an empty response.
}
resp = s.replyCompressed(req)
resp.Answer = ans
return resp
}
// genAnswersWithIPv4s generates DNS A answers provided IPv4 addresses. If any
// of the IPs isn't an IPv4 address, genAnswersWithIPv4s logs a warning and
// returns nil,
func (s *Server) genAnswersWithIPv4s(req *dns.Msg, ips []netip.Addr) (ans []dns.RR) {
for _, ip := range ips {
if !ip.Is4() {
log.Info("dnsforward: warning: ip %s is not ipv4 address", ip)
return nil
}
ans = append(ans, s.genAnswerA(req, ip))
}
return ans
}
// makeResponseNullIP creates a response with 0.0.0.0 for A requests, :: for
// AAAA requests, and an empty response for other types.
func (s *Server) makeResponseNullIP(req *dns.Msg) (resp *dns.Msg) {
// Respond with the corresponding zero IP type as opposed to simply
// using one or the other in both cases, because the IPv4 zero IP is
// converted to a IPV6-mapped IPv4 address, while the IPv6 zero IP is
// converted into an empty slice instead of the zero IPv4.
switch req.Question[0].Qtype {
case dns.TypeA:
resp = s.genResponseWithIPs(req, []netip.Addr{netip.IPv4Unspecified()})
case dns.TypeAAAA:
resp = s.genResponseWithIPs(req, []netip.Addr{netip.IPv6Unspecified()})
default:
resp = s.replyCompressed(req)
}
return resp
}
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg {
if newAddr == "" {
log.Info("dnsforward: block host is not specified")
return s.NewMsgSERVFAIL(request)
}
ip, err := netip.ParseAddr(newAddr)
if err == nil {
return s.genResponseWithIPs(request, []netip.Addr{ip})
}
// look up the hostname, TODO: cache
replReq := dns.Msg{}
replReq.SetQuestion(dns.Fqdn(newAddr), request.Question[0].Qtype)
replReq.RecursionDesired = true
newContext := &proxy.DNSContext{
Proto: d.Proto,
Addr: d.Addr,
Req: &replReq,
}
prx := s.proxy()
if prx == nil {
log.Debug("dnsforward: %s", srvClosedErr)
return s.NewMsgSERVFAIL(request)
}
err = prx.Resolve(newContext)
if err != nil {
log.Info("dnsforward: looking up replacement host %q: %s", newAddr, err)
return s.NewMsgSERVFAIL(request)
}
resp := s.replyCompressed(request)
if newContext.Res != nil {
for _, answer := range newContext.Res.Answer {
answer.Header().Name = request.Question[0].Name
resp.Answer = append(resp.Answer, answer)
}
}
return resp
}
// Create REFUSED DNS response
func (s *Server) makeResponseREFUSED(req *dns.Msg) *dns.Msg {
return s.reply(req, dns.RcodeRefused)
}
// type check
var _ proxy.MessageConstructor = (*Server)(nil)
// NewMsgNXDOMAIN implements the [proxy.MessageConstructor] interface for
// *Server.
func (s *Server) NewMsgNXDOMAIN(req *dns.Msg) (resp *dns.Msg) {
resp = s.reply(req, dns.RcodeNameError)
resp.Ns = s.genSOA(req)
return resp
}
// NewMsgSERVFAIL implements the [proxy.MessageConstructor] interface for
// *Server.
func (s *Server) NewMsgSERVFAIL(req *dns.Msg) (resp *dns.Msg) {
return s.reply(req, dns.RcodeServerFailure)
}
// NewMsgNOTIMPLEMENTED implements the [proxy.MessageConstructor] interface for
// *Server.
func (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) {
resp = s.reply(req, dns.RcodeNotImplemented)
// Most of the Internet and especially the inner core has an MTU of at least
// 1500 octets. Maximum DNS/UDP payload size for IPv6 on MTU 1500 ethernet
// is 1452 (1500 minus 40 (IPv6 header size) minus 8 (UDP header size)).
//
// See appendix A of https://datatracker.ietf.org/doc/draft-ietf-dnsop-avoid-fragmentation/17.
const maxUDPPayload = 1452
// NOTIMPLEMENTED without EDNS is treated as 'we don't support EDNS', so
// explicitly set it.
resp.SetEdns0(maxUDPPayload, false)
return resp
}
// NewMsgNODATA implements the [proxy.MessageConstructor] interface for *Server.
func (s *Server) NewMsgNODATA(req *dns.Msg) (resp *dns.Msg) {
resp = s.reply(req, dns.RcodeSuccess)
resp.Ns = s.genSOA(req)
return resp
}
func (s *Server) genSOA(req *dns.Msg) []dns.RR {
zone := ""
if len(req.Question) > 0 {
zone = req.Question[0].Name
}
const defaultBlockedResponseTTL = 3600
soa := dns.SOA{
// Values copied from verisign's nonexistent.com domain.
//
// Their exact values are not important in our use case because they are
// used for domain transfers between primary/secondary DNS servers.
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
// copied from AdGuard DNS
Ns: "fake-for-negative-caching.adguard.com.",
Serial: 100500,
// rest is request-specific
Hdr: dns.RR_Header{
Name: zone,
Rrtype: dns.TypeSOA,
Ttl: s.dnsFilter.BlockedResponseTTL(),
Class: dns.ClassINET,
},
// zone will be appended later if it's not ".".
Mbox: "hostmaster.",
}
if soa.Hdr.Ttl == 0 {
soa.Hdr.Ttl = defaultBlockedResponseTTL
}
if zone != "." {
soa.Mbox += zone
}
return []dns.RR{&soa}
}