Add support to detect upstream DNS resolver blocking

This commit is contained in:
Patrick Pacher
2020-04-17 17:02:04 +02:00
parent bffe4a9eaf
commit 9deb1623d6
7 changed files with 194 additions and 85 deletions

View File

@@ -246,8 +246,14 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
if err != nil {
// TODO: analyze nxdomain requests, malware could be trying DGA-domains
tracer.Warningf("nameserver: %s requested %s%s: %s", conn.Process(), q.FQDN, q.QType, err)
if _, ok := err.(*resolver.BlockedUpstreamError); ok {
conn.Block(err.Error())
} else {
conn.Failed("failed to resolve: " + err.Error())
}
returnNXDomain(w, query, conn.Reason)
conn.Failed("failed to resolve: " + err.Error())
return nil
}
@@ -261,6 +267,51 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
return nil
}
updateIPsAndCNAMEs(q, rrCache, conn)
// if we have CNAMEs and the profile is configured to filter them
// we need to re-check the lists and endpoints here
if conn.Process().Profile().FilterCNAMEs() {
conn.Entity.ResetLists()
conn.Entity.EnableCNAMECheck(true)
result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity)
if result == endpoints.Denied {
conn.Block("endpoint in blocklist: " + reason)
returnNXDomain(w, query, conn.Reason)
return nil
}
if result == endpoints.NoMatch {
result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity)
if result == endpoints.Denied {
conn.Block("endpoint in filterlists: " + reason)
returnNXDomain(w, query, conn.Reason)
return nil
}
}
}
// reply to query
m := new(dns.Msg)
m.SetReply(query)
m.Answer = rrCache.Answer
m.Ns = rrCache.Ns
m.Extra = rrCache.Extra
if err := w.WriteMsg(m); err != nil {
log.Warningf("nameserver: failed to return response %s%s to %s: %s", q.FQDN, q.QType, conn.Process(), err)
} else {
tracer.Debugf("nameserver: returning response %s%s to %s", q.FQDN, q.QType, conn.Process())
}
// save dns request as open
network.SaveOpenDNSRequest(conn)
return nil
}
func updateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *network.Connection) {
// save IP addresses to IPInfo
cnames := make(map[string]string)
ips := make(map[string]struct{})
@@ -322,45 +373,4 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
}
}
}
// if we have CNAMEs and the profile is configured to filter them
// we need to re-check the lists and endpoints here
if conn.Process().Profile().FilterCNAMEs() {
conn.Entity.ResetLists()
conn.Entity.EnableCNAMECheck(true)
result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity)
if result == endpoints.Denied {
conn.Block("endpoint in blocklist: " + reason)
returnNXDomain(w, query, conn.Reason)
return nil
}
if result == endpoints.NoMatch {
result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity)
if result == endpoints.Denied {
conn.Block("endpoint in filterlists: " + reason)
returnNXDomain(w, query, conn.Reason)
return nil
}
}
}
// reply to query
m := new(dns.Msg)
m.SetReply(query)
m.Answer = rrCache.Answer
m.Ns = rrCache.Ns
m.Extra = rrCache.Extra
if err := w.WriteMsg(m); err != nil {
log.Warningf("nameserver: failed to return response %s%s to %s: %s", q.FQDN, q.QType, conn.Process(), err)
} else {
tracer.Debugf("nameserver: returning response %s%s to %s", q.FQDN, q.QType, conn.Process())
}
// save dns request as open
network.SaveOpenDNSRequest(conn)
return nil
}

View File

@@ -73,32 +73,7 @@ func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason
if entity.CNAMECheckEnabled() {
for _, domain := range entity.CNAME {
switch ep.MatchType {
case domainMatchTypeExact:
if domain == ep.Domain {
result, reason = ep.matchesPPP(entity), ep.Reason
}
case domainMatchTypeZone:
if domain == ep.Domain {
result, reason = ep.matchesPPP(entity), ep.Reason
}
if strings.HasSuffix(domain, ep.DomainZone) {
result, reason = ep.matchesPPP(entity), ep.Reason
}
case domainMatchTypeSuffix:
if strings.HasSuffix(domain, ep.Domain) {
result, reason = ep.matchesPPP(entity), ep.Reason
}
case domainMatchTypePrefix:
if strings.HasPrefix(domain, ep.Domain) {
result, reason = ep.matchesPPP(entity), ep.Reason
}
case domainMatchTypeContains:
if strings.Contains(domain, ep.Domain) {
result, reason = ep.matchesPPP(entity), ep.Reason
}
}
result, reason = ep.check(entity, domain)
if result == Denied {
return result, reason
}

View File

@@ -0,0 +1,61 @@
package resolver
import (
"net"
"github.com/miekg/dns"
)
// Supported upstream block detections
const (
BlockDetectionRefused = "refused"
BlockDetectionZeroIP = "zeroip"
BlockDetectionEmptyAnswer = "empty"
BlockDetectionDisabled = "disabled"
)
func isBlockedUpstream(resolver *Resolver, answer *dns.Msg) bool {
if resolver.UpstreamBlockDetection == BlockDetectionDisabled {
return false
}
switch resolver.UpstreamBlockDetection {
case BlockDetectionRefused:
return answer.Rcode == dns.RcodeRefused
case BlockDetectionZeroIP:
if answer.Rcode != dns.RcodeSuccess {
return false
}
var ips []net.IP
for _, rr := range answer.Answer {
switch v := rr.(type) {
case *dns.A:
ips = append(ips, v.A)
case *dns.AAAA:
ips = append(ips, v.AAAA)
}
}
if len(ips) == 0 {
return false // we expected an empty IP
}
for _, ip := range ips {
if ip.To4() != nil {
if !ip.Equal(net.IPv4zero) {
return false
}
} else {
if !ip.To16().Equal(net.IPv6zero) {
return false
}
}
}
return true
case BlockDetectionEmptyAnswer:
return answer.Rcode == dns.RcodeNameError && len(answer.Ns) == 0 && len(answer.Answer) == 0 && len(answer.Extra) == 0
}
return false
}

View File

@@ -29,28 +29,30 @@ var (
// We encourage everyone who has the technical abilities to set their own preferred servers.
// Default 1: Cloudflare
"dot://1.1.1.1:853?verify=cloudflare-dns.com", // Cloudflare
"dot://1.0.0.1:853?verify=cloudflare-dns.com", // Cloudflare
"dot://1.1.1.1:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip", // Cloudflare
"dot://1.0.0.1:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip", // Cloudflare
// Default 2: Quad9
"dot://9.9.9.9:853?verify=dns.quad9.net", // Quad9
"dot://149.112.112.112:853?verify=dns.quad9.net", // Quad9
"dot://9.9.9.9:853?verify=dns.quad9.net&name=Quad9&blockedif=empty", // Quad9
"dot://149.112.112.112:853?verify=dns.quad9.net&name=Quad9&blockedif=empty", // Quad9
// Fallback 1: Cloudflare
"dns://1.1.1.1:53", // Cloudflare
"dns://1.0.0.1:53", // Cloudflare
"dns://1.1.1.1:53?name=Cloudflare&blockedif=zeroip", // Cloudflare
"dns://1.0.0.1:53?name=Cloudflare&blockedif=zeroip", // Cloudflare
// Fallback 2: Quad9
"dns://9.9.9.9:53", // Quad9
"dns://149.112.112.112:53", // Quad9
"dns://9.9.9.9:53?name=Quad9&blockedif=empty", // Quad9
"dns://149.112.112.112:53?name=Quad9&blockedif=empty", // Quad9
// supported parameters
// - `verify=domain`: verify domain (dot only)
// future parameters:
//
// - `name=name`: human readable name for resolver
// - `blockedif=baredns`: how to detect if the dns service blocked something
// - `baredns`: NXDomain result, but without any other record in any section
// - `blockedif=empty`: how to detect if the dns service blocked something
// - `empty`: NXDomain result, but without any other record in any section
// - `refused`: Request was refused
// - `zeroip`: Answer only contains zeroip
}
CfgOptionNameServersKey = "dns/nameservers"

View File

@@ -37,6 +37,21 @@ var (
ErrNoCompliance = fmt.Errorf("%w: no compliant resolvers for this query", ErrBlocked)
)
// BlockedUpstreamError is returned when a DNS request
// has been blocked by the upstream server.
type BlockedUpstreamError struct {
ResolverName string
}
func (blocked *BlockedUpstreamError) Error() string {
return fmt.Sprintf("Endpoint blocked by upstream DNS resolver %s", blocked.ResolverName)
}
// Unwrap implements errors.Unwrapper
func (blocked *BlockedUpstreamError) Unwrap() error {
return ErrBlocked
}
// Query describes a dns query.
type Query struct {
FQDN string

View File

@@ -28,6 +28,19 @@ type Resolver struct {
// Server config url (and ID)
Server string
// Name is the name of the resolver as passed via
// ?name=.
Name string
// UpstreamBlockDetection defines the detection type
// to identifier upstream DNS query blocking.
// Valid values are:
// - zeroip
// - empty
// - refused (default)
// - disabled
UpstreamBlockDetection string
// Parsed config
ServerType string
ServerAddress string
@@ -46,9 +59,25 @@ type Resolver struct {
Conn ResolverConn
}
// IsBlockedUpstream returns true if the request has been blocked
// upstream.
func (resolver *Resolver) IsBlockedUpstream(answer *dns.Msg) bool {
return isBlockedUpstream(resolver, answer)
}
// GetName returns the name of the server. If no name
// is configured the server address is returned.
func (resolver *Resolver) GetName() string {
if resolver.Name != "" {
return resolver.Name
}
return resolver.Server
}
// String returns the URL representation of the resolver.
func (resolver *Resolver) String() string {
return resolver.Server
return resolver.GetName()
}
// ResolverConn is an interface to implement different types of query backends.
@@ -126,6 +155,10 @@ func (brc *BasicResolverConn) Query(ctx context.Context, q *Query) (*RRCache, er
break
}
if resolver.IsBlockedUpstream(reply) {
return nil, &BlockedUpstreamError{resolver.GetName()}
}
// no error
break
}

View File

@@ -107,13 +107,26 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
return nil, false, fmt.Errorf("DOT must have a verify query parameter set")
}
blockType := query.Get("blockedif")
if blockType == "" {
blockType = BlockDetectionRefused
}
switch blockType {
case BlockDetectionDisabled, BlockDetectionEmptyAnswer, BlockDetectionRefused, BlockDetectionZeroIP:
default:
return nil, false, fmt.Errorf("invalid value for upstream block detection (blockedif=)")
}
new := &Resolver{
Server: resolverURL,
ServerType: u.Scheme,
ServerAddress: u.Host,
ServerIPScope: scope,
Source: source,
VerifyDomain: verifyDomain,
Server: resolverURL,
ServerType: u.Scheme,
ServerAddress: u.Host,
ServerIPScope: scope,
Source: source,
VerifyDomain: verifyDomain,
Name: query.Get("name"),
UpstreamBlockDetection: blockType,
}
newConn := &BasicResolverConn{