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 {
|
if err != nil {
|
||||||
// TODO: analyze nxdomain requests, malware could be trying DGA-domains
|
// 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)
|
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)
|
returnNXDomain(w, query, conn.Reason)
|
||||||
conn.Failed("failed to resolve: " + err.Error())
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +267,51 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
|||||||
return nil
|
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
|
// save IP addresses to IPInfo
|
||||||
cnames := make(map[string]string)
|
cnames := make(map[string]string)
|
||||||
ips := make(map[string]struct{})
|
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() {
|
if entity.CNAMECheckEnabled() {
|
||||||
for _, domain := range entity.CNAME {
|
for _, domain := range entity.CNAME {
|
||||||
switch ep.MatchType {
|
result, reason = ep.check(entity, domain)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if result == Denied {
|
if result == Denied {
|
||||||
return result, reason
|
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.
|
// We encourage everyone who has the technical abilities to set their own preferred servers.
|
||||||
|
|
||||||
// Default 1: Cloudflare
|
// Default 1: Cloudflare
|
||||||
"dot://1.1.1.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", // Cloudflare
|
"dot://1.0.0.1:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip", // Cloudflare
|
||||||
|
|
||||||
// Default 2: Quad9
|
// Default 2: Quad9
|
||||||
"dot://9.9.9.9: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", // Quad9
|
"dot://149.112.112.112:853?verify=dns.quad9.net&name=Quad9&blockedif=empty", // Quad9
|
||||||
|
|
||||||
// Fallback 1: Cloudflare
|
// Fallback 1: Cloudflare
|
||||||
"dns://1.1.1.1:53", // Cloudflare
|
"dns://1.1.1.1:53?name=Cloudflare&blockedif=zeroip", // Cloudflare
|
||||||
"dns://1.0.0.1:53", // Cloudflare
|
"dns://1.0.0.1:53?name=Cloudflare&blockedif=zeroip", // Cloudflare
|
||||||
|
|
||||||
// Fallback 2: Quad9
|
// Fallback 2: Quad9
|
||||||
"dns://9.9.9.9:53", // Quad9
|
"dns://9.9.9.9:53?name=Quad9&blockedif=empty", // Quad9
|
||||||
"dns://149.112.112.112:53", // Quad9
|
"dns://149.112.112.112:53?name=Quad9&blockedif=empty", // Quad9
|
||||||
|
|
||||||
// supported parameters
|
// supported parameters
|
||||||
// - `verify=domain`: verify domain (dot only)
|
// - `verify=domain`: verify domain (dot only)
|
||||||
// future parameters:
|
// future parameters:
|
||||||
//
|
//
|
||||||
// - `name=name`: human readable name for resolver
|
// - `name=name`: human readable name for resolver
|
||||||
// - `blockedif=baredns`: how to detect if the dns service blocked something
|
// - `blockedif=empty`: how to detect if the dns service blocked something
|
||||||
// - `baredns`: NXDomain result, but without any other record in any section
|
// - `empty`: NXDomain result, but without any other record in any section
|
||||||
|
// - `refused`: Request was refused
|
||||||
|
// - `zeroip`: Answer only contains zeroip
|
||||||
}
|
}
|
||||||
|
|
||||||
CfgOptionNameServersKey = "dns/nameservers"
|
CfgOptionNameServersKey = "dns/nameservers"
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ var (
|
|||||||
ErrNoCompliance = fmt.Errorf("%w: no compliant resolvers for this query", ErrBlocked)
|
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.
|
// Query describes a dns query.
|
||||||
type Query struct {
|
type Query struct {
|
||||||
FQDN string
|
FQDN string
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ type Resolver struct {
|
|||||||
// Server config url (and ID)
|
// Server config url (and ID)
|
||||||
Server string
|
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
|
// Parsed config
|
||||||
ServerType string
|
ServerType string
|
||||||
ServerAddress string
|
ServerAddress string
|
||||||
@@ -46,9 +59,25 @@ type Resolver struct {
|
|||||||
Conn ResolverConn
|
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.
|
// String returns the URL representation of the resolver.
|
||||||
func (resolver *Resolver) String() string {
|
func (resolver *Resolver) String() string {
|
||||||
return resolver.Server
|
return resolver.GetName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolverConn is an interface to implement different types of query backends.
|
// 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
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resolver.IsBlockedUpstream(reply) {
|
||||||
|
return nil, &BlockedUpstreamError{resolver.GetName()}
|
||||||
|
}
|
||||||
|
|
||||||
// no error
|
// no error
|
||||||
break
|
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")
|
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{
|
new := &Resolver{
|
||||||
Server: resolverURL,
|
Server: resolverURL,
|
||||||
ServerType: u.Scheme,
|
ServerType: u.Scheme,
|
||||||
ServerAddress: u.Host,
|
ServerAddress: u.Host,
|
||||||
ServerIPScope: scope,
|
ServerIPScope: scope,
|
||||||
Source: source,
|
Source: source,
|
||||||
VerifyDomain: verifyDomain,
|
VerifyDomain: verifyDomain,
|
||||||
|
Name: query.Get("name"),
|
||||||
|
UpstreamBlockDetection: blockType,
|
||||||
}
|
}
|
||||||
|
|
||||||
newConn := &BasicResolverConn{
|
newConn := &BasicResolverConn{
|
||||||
|
|||||||
Reference in New Issue
Block a user