Add support to detect upstream DNS resolver blocking
This commit is contained in:
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