AdGuardHome/internal/dnsforward/msg.go
Dimitry Kolyshev 65b526b969 Pull request: 5972-ip-dupl-ans
Updates #5972.

Squashed commit of the following:

commit 0e089f9ff8fd7e6d7cb53aa7c3b92435d1d41a81
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jul 11 15:33:16 2023 +0300

    dnsforward: imp code

commit 39527c078fd9ad6ea4906659e185d54e74ef6465
Merge: 03641b0b5 61ed74374
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jul 11 11:29:19 2023 +0300

    Merge remote-tracking branch 'origin/master' into 5972-ip-dupl-ans

    # Conflicts:
    #	CHANGELOG.md

commit 03641b0b511f8e48d386be76d0a4776296cf047d
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jul 10 14:03:28 2023 +0300

    all: dupl ips in answer
2023-07-11 15:46:01 +03:00

357 lines
9.2 KiB
Go

package dnsforward
import (
"net"
"time"
"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"
)
// makeResponse creates a DNS response by req and sets necessary flags. It also
// guarantees that req.Question will be not empty.
func (s *Server) makeResponse(req *dns.Msg) (resp *dns.Msg) {
resp = &dns.Msg{
MsgHdr: dns.MsgHdr{
RecursionAvailable: true,
},
Compress: true,
}
resp.SetReply(req)
return resp
}
// containsIP returns true if the IP is already in the list.
func containsIP(ips []net.IP, ip net.IP) bool {
for _, a := range ips {
if a.Equal(ip) {
return true
}
}
return false
}
// ipsFromRules extracts unique non-IP addresses from the filtering result
// rules.
func ipsFromRules(resRules []*filtering.ResultRule) (ips []net.IP) {
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 != nil && !containsIP(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
if qt := req.Question[0].Qtype; qt != dns.TypeA && qt != dns.TypeAAAA {
if s.conf.BlockingMode == BlockingModeNullIP {
return s.makeResponse(req)
}
return s.genNXDomain(req)
}
switch res.Reason {
case filtering.FilteredSafeBrowsing:
return s.genBlockedHost(req, s.conf.SafeBrowsingBlockHost, dctx)
case filtering.FilteredParental:
return s.genBlockedHost(req, s.conf.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.genResponseWithIPs(req, ipsFromRules(res.Rules))
default:
return s.genForBlockingMode(req, ipsFromRules(res.Rules))
}
}
// genForBlockingMode generates a filtered response to req based on the server's
// blocking mode.
func (s *Server) genForBlockingMode(req *dns.Msg, ips []net.IP) (resp *dns.Msg) {
qt := req.Question[0].Qtype
switch m := s.conf.BlockingMode; m {
case BlockingModeCustomIP:
switch qt {
case dns.TypeA:
return s.genARecord(req, s.conf.BlockingIPv4)
case dns.TypeAAAA:
return s.genAAAARecord(req, s.conf.BlockingIPv6)
default:
// Generally shouldn't happen, since the types are checked in
// genDNSFilterMessage.
log.Error("dns: invalid msg type %s for blocking mode %s", dns.Type(qt), m)
return s.makeResponse(req)
}
case BlockingModeDefault:
if len(ips) > 0 {
return s.genResponseWithIPs(req, ips)
}
return s.makeResponseNullIP(req)
case BlockingModeNullIP:
return s.makeResponseNullIP(req)
case BlockingModeNXDOMAIN:
return s.genNXDomain(req)
case BlockingModeREFUSED:
return s.makeResponseREFUSED(req)
default:
log.Error("dns: invalid blocking mode %q", s.conf.BlockingMode)
return s.makeResponse(req)
}
}
func (s *Server) genServerFailure(request *dns.Msg) *dns.Msg {
resp := dns.Msg{}
resp.SetRcode(request, dns.RcodeServerFailure)
resp.RecursionAvailable = true
return &resp
}
func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := s.makeResponse(request)
resp.Answer = append(resp.Answer, s.genAnswerA(request, ip))
return resp
}
func (s *Server) genAAAARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := s.makeResponse(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.conf.BlockedResponseTTL,
Class: dns.ClassINET,
}
}
func (s *Server) genAnswerA(req *dns.Msg, ip net.IP) (ans *dns.A) {
return &dns.A{
Hdr: s.hdr(req, dns.TypeA),
A: ip,
}
}
func (s *Server) genAnswerAAAA(req *dns.Msg, ip net.IP) (ans *dns.AAAA) {
return &dns.AAAA{
Hdr: s.hdr(req, dns.TypeAAAA),
AAAA: ip,
}
}
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 []net.IP) (resp *dns.Msg) {
var ans []dns.RR
switch req.Question[0].Qtype {
case dns.TypeA:
for _, ip := range ips {
if ip4 := ip.To4(); ip4 == nil {
ans = nil
break
}
ans = append(ans, s.genAnswerA(req, ip))
}
case dns.TypeAAAA:
for _, ip := range ips {
ans = append(ans, s.genAnswerAAAA(req, ip.To16()))
}
default:
// Go on and return an empty response.
}
resp = s.makeResponse(req)
resp.Answer = ans
return resp
}
// 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, []net.IP{{0, 0, 0, 0}})
case dns.TypeAAAA:
resp = s.genResponseWithIPs(req, []net.IP{net.IPv6zero})
default:
resp = s.makeResponse(req)
}
return resp
}
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg {
ip := net.ParseIP(newAddr)
if ip != nil {
return s.genResponseWithIPs(request, []net.IP{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,
StartTime: time.Now(),
Req: &replReq,
}
prx := s.proxy()
if prx == nil {
log.Debug("dns: %s", srvClosedErr)
return s.genServerFailure(request)
}
err := prx.Resolve(newContext)
if err != nil {
log.Printf("couldn't look up replacement host %q: %s", newAddr, err)
return s.genServerFailure(request)
}
resp := s.makeResponse(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
}
// preBlockedResponse returns a protocol-appropriate response for a request that
// was blocked by access settings.
func (s *Server) preBlockedResponse(pctx *proxy.DNSContext) (reply bool, err error) {
if pctx.Proto == proxy.ProtoUDP || pctx.Proto == proxy.ProtoDNSCrypt {
// Return nil so that dnsproxy drops the connection and thus
// prevent DNS amplification attacks.
return false, nil
}
pctx.Res = s.makeResponseREFUSED(pctx.Req)
return true, nil
}
// Create REFUSED DNS response
func (s *Server) makeResponseREFUSED(request *dns.Msg) *dns.Msg {
resp := dns.Msg{}
resp.SetRcode(request, dns.RcodeRefused)
resp.RecursionAvailable = true
return &resp
}
func (s *Server) genNXDomain(request *dns.Msg) *dns.Msg {
resp := dns.Msg{}
resp.SetRcode(request, dns.RcodeNameError)
resp.RecursionAvailable = true
resp.Ns = s.genSOA(request)
return &resp
}
func (s *Server) genSOA(request *dns.Msg) []dns.RR {
zone := ""
if len(request.Question) > 0 {
zone = request.Question[0].Name
}
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.conf.BlockedResponseTTL,
Class: dns.ClassINET,
},
Mbox: "hostmaster.", // zone will be appended later if it's not empty or "."
}
if soa.Hdr.Ttl == 0 {
soa.Hdr.Ttl = defaultValues.BlockedResponseTTL
}
if len(zone) > 0 && zone[0] != '.' {
soa.Mbox += zone
}
return []dns.RR{&soa}
}