178 lines
4.2 KiB
Go
178 lines
4.2 KiB
Go
// DNS Rebinding protection
|
|
|
|
package dnsforward
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
type dnsRebindChecker struct {
|
|
}
|
|
|
|
// IsPrivate reports whether ip is a private address, according to
|
|
// RFC 1918 (IPv4 addresses) and RFC 4193 (IPv6 addresses).
|
|
func (*dnsRebindChecker) isPrivate(ip net.IP) bool {
|
|
//TODO: remove once https://github.com/golang/go/pull/42793 makes it to stdlib
|
|
if ip4 := ip.To4(); ip4 != nil {
|
|
return ip4[0] == 10 ||
|
|
(ip4[0] == 172 && ip4[1]&0xf0 == 16) ||
|
|
(ip4[0] == 192 && ip4[1] == 168)
|
|
}
|
|
return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc
|
|
}
|
|
|
|
func (c *dnsRebindChecker) isRebindHost(host string) bool {
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return c.isRebindIP(ip)
|
|
}
|
|
|
|
return host == "localhost"
|
|
}
|
|
|
|
func (c *dnsRebindChecker) isRebindIP(ip net.IP) bool {
|
|
// This is compatible with dnsmasq definition
|
|
// See: https://github.com/imp/dnsmasq/blob/4e7694d7107d2299f4aaededf8917fceb5dfb924/src/rfc1035.c#L412
|
|
|
|
rebind := false
|
|
if ip4 := ip.To4(); ip4 != nil {
|
|
|
|
/* 0.0.0.0/8 (RFC 5735 section 3. "here" network) */
|
|
rebind = ip4[0] == 0 ||
|
|
|
|
/* 10.0.0.0/8 (private) */
|
|
ip4[0] == 10 ||
|
|
|
|
/* 172.16.0.0/12 (private) */
|
|
(ip4[0] == 172 && ip4[1]&0x10 == 0x10) ||
|
|
|
|
/* 169.254.0.0/16 (zeroconf) */
|
|
(ip4[0] == 169 && ip4[1] == 254) ||
|
|
|
|
/* 192.0.2.0/24 (test-net) */
|
|
(ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 2) ||
|
|
|
|
/* 198.51.100.0/24(test-net) */
|
|
(ip4[0] == 198 && ip4[1] == 51 && ip4[2] == 100) ||
|
|
|
|
/* 203.0.113.0/24 (test-net) */
|
|
(ip4[0] == 203 && ip4[1] == 0 && ip4[2] == 113) ||
|
|
|
|
/* 255.255.255.255/32 (broadcast)*/
|
|
ip4.Equal(net.IPv4bcast)
|
|
} else {
|
|
rebind = ip.Equal(net.IPv6zero) || ip.Equal(net.IPv6unspecified) ||
|
|
ip.Equal(net.IPv6interfacelocalallnodes) ||
|
|
ip.Equal(net.IPv6linklocalallnodes) ||
|
|
ip.Equal(net.IPv6linklocalallrouters)
|
|
}
|
|
|
|
return rebind || c.isPrivate(ip) || ip.IsLoopback()
|
|
}
|
|
|
|
// Checks DNS rebinding attacks
|
|
// Note both whitelisted and cached hosts will bypass rebinding check (see: processFilteringAfterResponse()).
|
|
func (s *Server) isResponseRebind(domain, host string) bool {
|
|
if !s.conf.RebindingProtectionEnabled {
|
|
return false
|
|
}
|
|
|
|
if log.GetLevel() >= log.DEBUG {
|
|
timer := log.StartTimer()
|
|
defer timer.LogElapsed("DNS Rebinding check for %s -> %s", domain, host)
|
|
}
|
|
|
|
for _, h := range s.conf.RebindingAllowedHosts {
|
|
if strings.HasSuffix(domain, h) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
c := dnsRebindChecker{}
|
|
return c.isRebindHost(host)
|
|
}
|
|
|
|
func processRebindingFilteringAfterResponse(ctx *dnsContext) int {
|
|
s := ctx.srv
|
|
d := ctx.proxyCtx
|
|
res := ctx.result
|
|
var err error
|
|
|
|
if !ctx.responseFromUpstream || res.Reason == dnsfilter.ReasonRewrite {
|
|
return resultDone
|
|
}
|
|
|
|
originalRes := d.Res
|
|
ctx.result, err = s.preventRebindResponse(ctx)
|
|
if err != nil {
|
|
ctx.err = err
|
|
return resultError
|
|
}
|
|
if ctx.result != nil {
|
|
ctx.origResp = originalRes // matched by response
|
|
} else {
|
|
ctx.result = &dnsfilter.Result{}
|
|
}
|
|
|
|
return resultDone
|
|
}
|
|
|
|
func (s *Server) preventRebindResponse(ctx *dnsContext) (*dnsfilter.Result, error) {
|
|
d := ctx.proxyCtx
|
|
|
|
for _, a := range d.Res.Answer {
|
|
m := ""
|
|
domainName := ""
|
|
host := ""
|
|
|
|
switch v := a.(type) {
|
|
case *dns.CNAME:
|
|
host = strings.TrimSuffix(v.Target, ".")
|
|
domainName = v.Hdr.Name
|
|
m = fmt.Sprintf("DNSRebind: Checking CNAME %s for %s", v.Target, v.Hdr.Name)
|
|
|
|
case *dns.A:
|
|
host = v.A.String()
|
|
domainName = v.Hdr.Name
|
|
m = fmt.Sprintf("DNSRebind: Checking record A (%s) for %s", host, v.Hdr.Name)
|
|
|
|
case *dns.AAAA:
|
|
host = v.AAAA.String()
|
|
domainName = v.Hdr.Name
|
|
m = fmt.Sprintf("DNSRebind: Checking record AAAA (%s) for %s", host, v.Hdr.Name)
|
|
|
|
default:
|
|
continue
|
|
}
|
|
|
|
s.RLock()
|
|
if !s.conf.RebindingProtectionEnabled {
|
|
s.RUnlock()
|
|
continue
|
|
}
|
|
|
|
log.Debug(m)
|
|
blocked := s.isResponseRebind(domainName, host)
|
|
s.RUnlock()
|
|
|
|
if blocked {
|
|
res := &dnsfilter.Result{
|
|
IsFiltered: true,
|
|
Reason: dnsfilter.FilteredRebind,
|
|
Rule: "adguard-rebind-protection",
|
|
}
|
|
|
|
d.Res = s.genDNSFilterMessage(d, res)
|
|
log.Debug("DNSRebind: Matched %s by response: %s", d.Req.Question[0].Name, host)
|
|
return res, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|