mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-15 03:02:07 +03:00
762ef4a6db
Squashed commit of the following: commit 85ea3d985e83209e3b49119959aedd330df24d23 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 18 15:19:38 2024 +0200 all: imp docs commit b0695daddbcf191454c5e829ca4d19def8ddacbf Merge: a79f98f2f48c6242a7
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 17 11:06:49 2024 +0200 Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https # Conflicts: # CHANGELOG.md commit a79f98f2f215a4a79ca4d186c0da33db936429dc Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 17 11:05:34 2024 +0200 dnsforward: imp code commit b901a1169cc78313298d70cce770cd1523ccbf9f Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 16 11:03:52 2024 +0200 dnsforward: imp code commit fb6e66971b1b984147ec400ceaff856e7b5710c7 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 16 10:08:51 2024 +0200 all: safesearch rewrites commit 88add21831fff7e04539f5dd299832883a6f3995 Merge: b78ad8f74201ac73cf
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 16 09:43:20 2024 +0200 Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https # Conflicts: # CHANGELOG.md commit b78ad8f748c7fa52533e0541cae16bd51c201370 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Fri Apr 12 13:34:39 2024 +0200 all: safesearch rewrites commit fb3efbb053242c537ca872542006917b8e8ac1ff Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 11 13:15:37 2024 +0200 safesearch: imp code commit 1193c704f4d30be4a2cc66e84a31c9a6020ab269 Merge: 14e823d7cff7c715c5
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 11 13:13:44 2024 +0200 Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https # Conflicts: # CHANGELOG.md commit 14e823d7cc13c275c2ed04704883a94b95e29963 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 11 13:11:43 2024 +0200 all: safesearch https commit cd403a2897ae56a9059a78f24b104af5805d84ab Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 11 12:09:27 2024 +0200 Revert "all: safesearch https" This reverts commit 1c9564b9b4db70f85b2f827cc06b65d2b67b08b1. commit 1c9564b9b4db70f85b2f827cc06b65d2b67b08b1 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 10 12:41:47 2024 +0200 all: safesearch https commit 5f42688fbab566973acc8dc414a992819492a9ac Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 10 09:22:30 2024 +0200 filtering: imp code commit eb9bd9f47cd71cafe8eee4698a8a0d5d25dea3d3 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 10 09:19:22 2024 +0200 all: changelog commit 0c77c705a942fe83d3809a7efbc8a6baf5886762 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 10 08:55:22 2024 +0200 safesearch: imp tests commit 492a93fbb5ff54678e22a15577f509b2327c2ebe Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 9 14:45:16 2024 +0200 all: changelog commit a665e7246d11503c47d48ccc714e6862f764e930 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 9 14:41:24 2024 +0200 safesearch: https req
428 lines
12 KiB
Go
428 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)
|
|
}
|
|
|
|
// newMsgNODATA returns a properly initialized NODATA response.
|
|
//
|
|
// See https://www.rfc-editor.org/rfc/rfc2308#section-2.2.
|
|
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(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.dnsFilter.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 = defaultBlockedResponseTTL
|
|
}
|
|
if len(zone) > 0 && zone[0] != '.' {
|
|
soa.Mbox += zone
|
|
}
|
|
|
|
return []dns.RR{&soa}
|
|
}
|
|
|
|
// 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
|
|
}
|