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

@@ -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{