Add support to detect upstream DNS resolver blocking
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
61
resolver/block_detection.go
Normal file
61
resolver/block_detection.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user