diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index f02151fe..013fc749 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -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 } diff --git a/profile/endpoints/endpoint-domain.go b/profile/endpoints/endpoint-domain.go index 3350f053..2691ba13 100644 --- a/profile/endpoints/endpoint-domain.go +++ b/profile/endpoints/endpoint-domain.go @@ -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 } diff --git a/resolver/block_detection.go b/resolver/block_detection.go new file mode 100644 index 00000000..8a4005cd --- /dev/null +++ b/resolver/block_detection.go @@ -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 +} diff --git a/resolver/config.go b/resolver/config.go index d4c4828d..ac9567b0 100644 --- a/resolver/config.go +++ b/resolver/config.go @@ -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" diff --git a/resolver/resolve.go b/resolver/resolve.go index 2c7f532a..f13d07c2 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -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 diff --git a/resolver/resolver.go b/resolver/resolver.go index 0201b0c5..65155fab 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -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 } diff --git a/resolver/resolvers.go b/resolver/resolvers.go index b4451f11..00ad0d0e 100644 --- a/resolver/resolvers.go +++ b/resolver/resolvers.go @@ -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{