plugin/view: Advanced routing interface and new 'view' plugin (#5538)

* introduce new interface "dnsserver.Viewer", that allows a plugin implementing it to decide if a query should be routed into its server block.
* add new plugin "view", that uses the new interface to enable a user to define expression based conditions that must be met for a query to be routed to its server block.

Signed-off-by: Chris O'Haver <cohaver@infoblox.com>
This commit is contained in:
Chris O'Haver 2022-09-08 14:56:27 -04:00 committed by GitHub
parent 1f0a41a665
commit b56b080a7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 880 additions and 114 deletions

View file

@ -49,11 +49,23 @@ func newOverlapZone() *zoneOverlap {
// registerAndCheck adds a new zoneAddr for validation, it returns information about existing or overlapping with already registered // registerAndCheck adds a new zoneAddr for validation, it returns information about existing or overlapping with already registered
// we consider that an unbound address is overlapping all bound addresses for same zone, same port // we consider that an unbound address is overlapping all bound addresses for same zone, same port
func (zo *zoneOverlap) registerAndCheck(z zoneAddr) (existingZone *zoneAddr, overlappingZone *zoneAddr) { func (zo *zoneOverlap) registerAndCheck(z zoneAddr) (existingZone *zoneAddr, overlappingZone *zoneAddr) {
existingZone, overlappingZone = zo.check(z)
if existingZone != nil || overlappingZone != nil {
return existingZone, overlappingZone
}
// there is no overlap, keep the current zoneAddr for future checks
zo.registeredAddr[z] = z
zo.unboundOverlap[z.unbound()] = z
return nil, nil
}
// check validates a zoneAddr for overlap without registering it
func (zo *zoneOverlap) check(z zoneAddr) (existingZone *zoneAddr, overlappingZone *zoneAddr) {
if exist, ok := zo.registeredAddr[z]; ok { if exist, ok := zo.registeredAddr[z]; ok {
// exact same zone already registered // exact same zone already registered
return &exist, nil return &exist, nil
} }
uz := zoneAddr{Zone: z.Zone, Address: "", Port: z.Port, Transport: z.Transport} uz := z.unbound()
if already, ok := zo.unboundOverlap[uz]; ok { if already, ok := zo.unboundOverlap[uz]; ok {
if z.Address == "" { if z.Address == "" {
// current is not bound to an address, but there is already another zone with a bind address registered // current is not bound to an address, but there is already another zone with a bind address registered
@ -64,8 +76,11 @@ func (zo *zoneOverlap) registerAndCheck(z zoneAddr) (existingZone *zoneAddr, ove
return nil, &uz return nil, &uz
} }
} }
// there is no overlap, keep the current zoneAddr for future checks // there is no overlap
zo.registeredAddr[z] = z
zo.unboundOverlap[uz] = z
return nil, nil return nil, nil
} }
// unbound returns an unbound version of the zoneAddr
func (z zoneAddr) unbound() zoneAddr {
return zoneAddr{Zone: z.Zone, Address: "", Port: z.Port, Transport: z.Transport}
}

View file

@ -1,12 +1,14 @@
package dnsserver package dnsserver
import ( import (
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"github.com/coredns/caddy" "github.com/coredns/caddy"
"github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/request"
) )
// Config configuration for a single server. // Config configuration for a single server.
@ -40,6 +42,14 @@ type Config struct {
// may depend on it. // may depend on it.
HTTPRequestValidateFunc func(*http.Request) bool HTTPRequestValidateFunc func(*http.Request) bool
// FilterFuncs is used to further filter access
// to this handler. E.g. to limit access to a reverse zone
// on a non-octet boundary, i.e. /17
FilterFuncs []FilterFunc
// ViewName is the name of the Viewer PLugin defined in the Config
ViewName string
// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS). // TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS).
TLSConfig *tls.Config TLSConfig *tls.Config
@ -60,8 +70,14 @@ type Config struct {
// firstConfigInBlock is used to reference the first config in a server block, for the // firstConfigInBlock is used to reference the first config in a server block, for the
// purpose of sharing single instance of each plugin among all zones in a server block. // purpose of sharing single instance of each plugin among all zones in a server block.
firstConfigInBlock *Config firstConfigInBlock *Config
// metaCollector references the first MetadataCollector plugin, if one exists
metaCollector MetadataCollector
} }
// FilterFunc is a function that filters requests from the Config
type FilterFunc func(context.Context, *request.Request) bool
// keyForConfig builds a key for identifying the configs during setup time // keyForConfig builds a key for identifying the configs during setup time
func keyForConfig(blocIndex int, blocKeyIndex int) string { func keyForConfig(blocIndex int, blocKeyIndex int) string {
return fmt.Sprintf("%d:%d", blocIndex, blocKeyIndex) return fmt.Sprintf("%d:%d", blocIndex, blocKeyIndex)

View file

@ -21,7 +21,7 @@ func checkZoneSyntax(zone string) bool {
// startUpZones creates the text that we show when starting up: // startUpZones creates the text that we show when starting up:
// grpc://example.com.:1055 // grpc://example.com.:1055
// example.com.:1053 on 127.0.0.1 // example.com.:1053 on 127.0.0.1
func startUpZones(protocol, addr string, zones map[string]*Config) string { func startUpZones(protocol, addr string, zones map[string][]*Config) string {
s := "" s := ""
keys := make([]string, len(zones)) keys := make([]string, len(zones))

View file

@ -138,13 +138,6 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy
// MakeServers uses the newly-created siteConfigs to create and return a list of server instances. // MakeServers uses the newly-created siteConfigs to create and return a list of server instances.
func (h *dnsContext) MakeServers() ([]caddy.Server, error) { func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
// Now that all Keys and Directives are parsed and initialized
// lets verify that there is no overlap on the zones and addresses to listen for
errValid := h.validateZonesAndListeningAddresses()
if errValid != nil {
return nil, errValid
}
// Copy the Plugin, ListenHosts and Debug from first config in the block // Copy the Plugin, ListenHosts and Debug from first config in the block
// to all other config in the same block . Doing this results in zones // to all other config in the same block . Doing this results in zones
// sharing the same plugin instances and settings as other zones in // sharing the same plugin instances and settings as other zones in
@ -198,6 +191,27 @@ func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
} }
} }
// For each server config, check for View Filter plugins
for _, c := range h.configs {
// Add filters in the plugin.cfg order for consistent filter func evaluation order.
for _, d := range Directives {
if vf, ok := c.registry[d].(Viewer); ok {
if c.ViewName != "" {
return nil, fmt.Errorf("multiple views defined in server block")
}
c.ViewName = vf.ViewName()
c.FilterFuncs = append(c.FilterFuncs, vf.Filter)
}
}
}
// Verify that there is no overlap on the zones and listen addresses
// for unfiltered server configs
errValid := h.validateZonesAndListeningAddresses()
if errValid != nil {
return nil, errValid
}
return servers, nil return servers, nil
} }
@ -253,7 +267,15 @@ func (h *dnsContext) validateZonesAndListeningAddresses() error {
for _, h := range conf.ListenHosts { for _, h := range conf.ListenHosts {
// Validate the overlapping of ZoneAddr // Validate the overlapping of ZoneAddr
akey := zoneAddr{Transport: conf.Transport, Zone: conf.Zone, Address: h, Port: conf.Port} akey := zoneAddr{Transport: conf.Transport, Zone: conf.Zone, Address: h, Port: conf.Port}
existZone, overlapZone := checker.registerAndCheck(akey) var existZone, overlapZone *zoneAddr
if len(conf.FilterFuncs) > 0 {
// This config has filters. Check for overlap with other (unfiltered) configs.
existZone, overlapZone = checker.check(akey)
} else {
// This config has no filters. Check for overlap with other (unfiltered) configs,
// and register the zone to prevent subsequent zones from overlapping with it.
existZone, overlapZone = checker.registerAndCheck(akey)
}
if existZone != nil { if existZone != nil {
return fmt.Errorf("cannot serve %s - it is already defined", akey.String()) return fmt.Errorf("cannot serve %s - it is already defined", akey.String())
} }

View file

@ -37,23 +37,28 @@ type Server struct {
server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case. server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case.
m sync.Mutex // protects the servers m sync.Mutex // protects the servers
zones map[string]*Config // zones keyed by their address zones map[string][]*Config // zones keyed by their address
dnsWg sync.WaitGroup // used to wait on outstanding connections dnsWg sync.WaitGroup // used to wait on outstanding connections
graceTimeout time.Duration // the maximum duration of a graceful shutdown graceTimeout time.Duration // the maximum duration of a graceful shutdown
trace trace.Trace // the trace plugin for the server trace trace.Trace // the trace plugin for the server
debug bool // disable recover() debug bool // disable recover()
stacktrace bool // enable stacktrace in recover error log stacktrace bool // enable stacktrace in recover error log
classChaos bool // allow non-INET class queries classChaos bool // allow non-INET class queries
tsigSecret map[string]string tsigSecret map[string]string
} }
// MetadataCollector is a plugin that can retrieve metadata functions from all metadata providing plugins
type MetadataCollector interface {
Collect(context.Context, request.Request) context.Context
}
// NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class // NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class
// queries are blocked unless queries from enableChaos are loaded. // queries are blocked unless queries from enableChaos are loaded.
func NewServer(addr string, group []*Config) (*Server, error) { func NewServer(addr string, group []*Config) (*Server, error) {
s := &Server{ s := &Server{
Addr: addr, Addr: addr,
zones: make(map[string]*Config), zones: make(map[string][]*Config),
graceTimeout: 5 * time.Second, graceTimeout: 5 * time.Second,
tsigSecret: make(map[string]string), tsigSecret: make(map[string]string),
} }
@ -72,8 +77,9 @@ func NewServer(addr string, group []*Config) (*Server, error) {
log.D.Set() log.D.Set()
} }
s.stacktrace = site.Stacktrace s.stacktrace = site.Stacktrace
// set the config per zone
s.zones[site.Zone] = site // append the config to the zone's configs
s.zones[site.Zone] = append(s.zones[site.Zone], site)
// copy tsig secrets // copy tsig secrets
for key, secret := range site.TsigSecret { for key, secret := range site.TsigSecret {
@ -88,6 +94,12 @@ func NewServer(addr string, group []*Config) (*Server, error) {
// register the *handler* also // register the *handler* also
site.registerHandler(stack) site.registerHandler(stack)
// If the current plugin is a MetadataCollector, bookmark it for later use. This loop traverses the plugin
// list backwards, so the first MetadataCollector plugin wins.
if mdc, ok := stack.(MetadataCollector); ok {
site.metaCollector = mdc
}
if s.trace == nil && stack.Name() == "trace" { if s.trace == nil && stack.Name() == "trace" {
// we have to stash away the plugin, not the // we have to stash away the plugin, not the
// Tracer object, because the Tracer won't be initialized yet // Tracer object, because the Tracer won't be initialized yet
@ -254,24 +266,39 @@ func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
) )
for { for {
if h, ok := s.zones[q[off:]]; ok { if z, ok := s.zones[q[off:]]; ok {
if h.pluginChain == nil { // zone defined, but has not got any plugins for _, h := range z {
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) if h.pluginChain == nil { // zone defined, but has not got any plugins
return errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
} return
if r.Question[0].Qtype != dns.TypeDS { }
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
if !plugin.ClientWrite(rcode) { if h.metaCollector != nil {
errorFunc(s.Addr, w, r, rcode) // Collect metadata now, so it can be used before we send a request down the plugin chain.
ctx = h.metaCollector.Collect(ctx, request.Request{Req: r, W: w})
}
// If all filter funcs pass, use this config.
if passAllFilterFuncs(ctx, h.FilterFuncs, &request.Request{Req: r, W: w}) {
if h.ViewName != "" {
// if there was a view defined for this Config, set the view name in the context
ctx = context.WithValue(ctx, ViewKey{}, h.ViewName)
}
if r.Question[0].Qtype != dns.TypeDS {
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
if !plugin.ClientWrite(rcode) {
errorFunc(s.Addr, w, r, rcode)
}
return
}
// The type is DS, keep the handler, but keep on searching as maybe we are serving
// the parent as well and the DS should be routed to it - this will probably *misroute* DS
// queries to a possibly grand parent, but there is no way for us to know at this point
// if there is an actual delegation from grandparent -> parent -> zone.
// In all fairness: direct DS queries should not be needed.
dshandler = h
} }
return
} }
// The type is DS, keep the handler, but keep on searching as maybe we are serving
// the parent as well and the DS should be routed to it - this will probably *misroute* DS
// queries to a possibly grand parent, but there is no way for us to know at this point
// if there is an actual delegation from grandparent -> parent -> zone.
// In all fairness: direct DS queries should not be needed.
dshandler = h
} }
off, end = dns.NextLabel(q, off) off, end = dns.NextLabel(q, off)
if end { if end {
@ -289,18 +316,46 @@ func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
} }
// Wildcard match, if we have found nothing try the root zone as a last resort. // Wildcard match, if we have found nothing try the root zone as a last resort.
if h, ok := s.zones["."]; ok && h.pluginChain != nil { if z, ok := s.zones["."]; ok {
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) for _, h := range z {
if !plugin.ClientWrite(rcode) { if h.pluginChain == nil {
errorFunc(s.Addr, w, r, rcode) continue
}
if h.metaCollector != nil {
// Collect metadata now, so it can be used before we send a request down the plugin chain.
ctx = h.metaCollector.Collect(ctx, request.Request{Req: r, W: w})
}
// If all filter funcs pass, use this config.
if passAllFilterFuncs(ctx, h.FilterFuncs, &request.Request{Req: r, W: w}) {
if h.ViewName != "" {
// if there was a view defined for this Config, set the view name in the context
ctx = context.WithValue(ctx, ViewKey{}, h.ViewName)
}
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
if !plugin.ClientWrite(rcode) {
errorFunc(s.Addr, w, r, rcode)
}
return
}
} }
return
} }
// Still here? Error out with REFUSED. // Still here? Error out with REFUSED.
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
} }
// passAllFilterFuncs returns true if all filter funcs evaluate to true for the given request
func passAllFilterFuncs(ctx context.Context, filterFuncs []FilterFunc, req *request.Request) bool {
for _, ff := range filterFuncs {
if !ff(ctx, req) {
return false
}
}
return true
}
// OnStartupComplete lists the sites served by this server // OnStartupComplete lists the sites served by this server
// and any relevant information, assuming Quiet is false. // and any relevant information, assuming Quiet is false.
func (s *Server) OnStartupComplete() { func (s *Server) OnStartupComplete() {
@ -341,7 +396,7 @@ func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int
answer.SetRcode(r, rc) answer.SetRcode(r, rc)
state.SizeAndDo(answer) state.SizeAndDo(answer)
vars.Report(server, state, vars.Dropped, rcode.ToString(rc), "" /* plugin */, answer.Len(), time.Now()) vars.Report(server, state, vars.Dropped, "", rcode.ToString(rc), "" /* plugin */, answer.Len(), time.Now())
w.WriteMsg(answer) w.WriteMsg(answer)
} }
@ -357,6 +412,9 @@ type (
// LoopKey is the context key to detect server wide loops. // LoopKey is the context key to detect server wide loops.
LoopKey struct{} LoopKey struct{}
// ViewKey is the context key for the current view, if defined
ViewKey struct{}
) )
// EnableChaos is a map with plugin names for which we should open CH class queries as we block these by default. // EnableChaos is a map with plugin names for which we should open CH class queries as we block these by default.

View file

@ -37,9 +37,11 @@ func NewServergRPC(addr string, group []*Config) (*ServergRPC, error) {
// The *tls* plugin must make sure that multiple conflicting // The *tls* plugin must make sure that multiple conflicting
// TLS configuration returns an error: it can only be specified once. // TLS configuration returns an error: it can only be specified once.
var tlsConfig *tls.Config var tlsConfig *tls.Config
for _, conf := range s.zones { for _, z := range s.zones {
// Should we error if some configs *don't* have TLS? for _, conf := range z {
tlsConfig = conf.TLSConfig // Should we error if some configs *don't* have TLS?
tlsConfig = conf.TLSConfig
}
} }
// http/2 is required when using gRPC. We need to specify it in next protos // http/2 is required when using gRPC. We need to specify it in next protos
// or the upgrade won't happen. // or the upgrade won't happen.

View file

@ -50,9 +50,11 @@ func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) {
// The *tls* plugin must make sure that multiple conflicting // The *tls* plugin must make sure that multiple conflicting
// TLS configuration returns an error: it can only be specified once. // TLS configuration returns an error: it can only be specified once.
var tlsConfig *tls.Config var tlsConfig *tls.Config
for _, conf := range s.zones { for _, z := range s.zones {
// Should we error if some configs *don't* have TLS? for _, conf := range z {
tlsConfig = conf.TLSConfig // Should we error if some configs *don't* have TLS?
tlsConfig = conf.TLSConfig
}
} }
// http/2 is recommended when using DoH. We need to specify it in next protos // http/2 is recommended when using DoH. We need to specify it in next protos
@ -63,8 +65,10 @@ func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) {
// Use a custom request validation func or use the standard DoH path check. // Use a custom request validation func or use the standard DoH path check.
var validator func(*http.Request) bool var validator func(*http.Request) bool
for _, conf := range s.zones { for _, z := range s.zones {
validator = conf.HTTPRequestValidateFunc for _, conf := range z {
validator = conf.HTTPRequestValidateFunc
}
} }
if validator == nil { if validator == nil {
validator = func(r *http.Request) bool { return r.URL.Path == doh.Path } validator = func(r *http.Request) bool { return r.URL.Path == doh.Path }

View file

@ -28,9 +28,11 @@ func NewServerTLS(addr string, group []*Config) (*ServerTLS, error) {
// The *tls* plugin must make sure that multiple conflicting // The *tls* plugin must make sure that multiple conflicting
// TLS configuration returns an error: it can only be specified once. // TLS configuration returns an error: it can only be specified once.
var tlsConfig *tls.Config var tlsConfig *tls.Config
for _, conf := range s.zones { for _, z := range s.zones {
// Should we error if some configs *don't* have TLS? for _, conf := range z {
tlsConfig = conf.TLSConfig // Should we error if some configs *don't* have TLS?
tlsConfig = conf.TLSConfig
}
} }
return &ServerTLS{Server: s, tlsConfig: tlsConfig}, nil return &ServerTLS{Server: s, tlsConfig: tlsConfig}, nil

20
core/dnsserver/view.go Normal file
View file

@ -0,0 +1,20 @@
package dnsserver
import (
"context"
"github.com/coredns/coredns/request"
)
// Viewer - If Viewer is implemented by a plugin in a server block, its Filter()
// is added to the server block's filter functions when starting the server. When a running server
// serves a DNS request, it will route the request to the first Config (server block) that passes
// all its filter functions.
type Viewer interface {
// Filter returns true if the server should use the server block in which the implementing plugin resides, and the
// name of the view for metrics logging.
Filter(ctx context.Context, req *request.Request) bool
// ViewName returns the name of the view
ViewName() string
}

View file

@ -60,4 +60,5 @@ var Directives = []string{
"whoami", "whoami",
"on", "on",
"sign", "sign",
"view",
} }

View file

@ -53,5 +53,6 @@ import (
_ "github.com/coredns/coredns/plugin/trace" _ "github.com/coredns/coredns/plugin/trace"
_ "github.com/coredns/coredns/plugin/transfer" _ "github.com/coredns/coredns/plugin/transfer"
_ "github.com/coredns/coredns/plugin/tsig" _ "github.com/coredns/coredns/plugin/tsig"
_ "github.com/coredns/coredns/plugin/view"
_ "github.com/coredns/coredns/plugin/whoami" _ "github.com/coredns/coredns/plugin/whoami"
) )

1
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible github.com/Azure/azure-sdk-for-go v66.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.28 github.com/Azure/go-autorest/autorest v0.11.28
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
github.com/antonmedv/expr v1.9.0
github.com/apparentlymart/go-cidr v1.1.0 github.com/apparentlymart/go-cidr v1.1.0
github.com/aws/aws-sdk-go v1.44.91 github.com/aws/aws-sdk-go v1.44.91
github.com/coredns/caddy v1.1.1 github.com/coredns/caddy v1.1.1

15
go.sum
View file

@ -93,6 +93,7 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 h1:3nVO1nQyh64IUY6BPZUpMYMZ738Pu+LsMt3E0eqqIYw= github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 h1:3nVO1nQyh64IUY6BPZUpMYMZ738Pu+LsMt3E0eqqIYw=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw= github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
@ -132,6 +133,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
@ -197,6 +200,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -261,6 +265,8 @@ github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/garyburd/redigo v1.6.3/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw= github.com/garyburd/redigo v1.6.3/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -623,6 +629,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -648,6 +656,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
@ -786,6 +796,8 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua
github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM= github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
@ -796,6 +808,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/kafka-go v0.4.29/go.mod h1:m1lXeqJtIFYZayv0shM/tjrAFljvWLTprxBHd+3PnaU= github.com/segmentio/kafka-go v0.4.29/go.mod h1:m1lXeqJtIFYZayv0shM/tjrAFljvWLTprxBHd+3PnaU=
@ -825,6 +838,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -1121,6 +1135,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View file

@ -69,3 +69,4 @@ erratic:erratic
whoami:whoami whoami:whoami
on:github.com/coredns/caddy/onevent on:github.com/coredns/caddy/onevent
sign:sign sign:sign
view:view

View file

@ -85,14 +85,14 @@ Entries with 0 TTL will remain in the cache until randomly evicted when the shar
If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported:
* `coredns_cache_entries{server, type, zones}` - Total elements in the cache by cache type. * `coredns_cache_entries{server, type, zones, view}` - Total elements in the cache by cache type.
* `coredns_cache_hits_total{server, type, zones}` - Counter of cache hits by cache type. * `coredns_cache_hits_total{server, type, zones, view}` - Counter of cache hits by cache type.
* `coredns_cache_misses_total{server, zones}` - Counter of cache misses. - Deprecated, derive misses from cache hits/requests counters. * `coredns_cache_misses_total{server, zones, view}` - Counter of cache misses. - Deprecated, derive misses from cache hits/requests counters.
* `coredns_cache_requests_total{server, zones}` - Counter of cache requests. * `coredns_cache_requests_total{server, zones, view}` - Counter of cache requests.
* `coredns_cache_prefetch_total{server, zones}` - Counter of times the cache has prefetched a cached item. * `coredns_cache_prefetch_total{server, zones, view}` - Counter of times the cache has prefetched a cached item.
* `coredns_cache_drops_total{server, zones}` - Counter of responses excluded from the cache due to request/response question name mismatch. * `coredns_cache_drops_total{server, zones, view}` - Counter of responses excluded from the cache due to request/response question name mismatch.
* `coredns_cache_served_stale_total{server, zones}` - Counter of requests served from stale cache entries. * `coredns_cache_served_stale_total{server, zones, view}` - Counter of requests served from stale cache entries.
* `coredns_cache_evictions_total{server, type, zones}` - Counter of cache evictions. * `coredns_cache_evictions_total{server, type, zones, view}` - Counter of cache evictions.
Cache types are either "denial" or "success". `Server` is the server handling the request, see the Cache types are either "denial" or "success". `Server` is the server handling the request, see the
prometheus plugin for documentation. prometheus plugin for documentation.

11
plugin/cache/cache.go vendored
View file

@ -22,6 +22,7 @@ type Cache struct {
Zones []string Zones []string
zonesMetricLabel string zonesMetricLabel string
viewMetricLabel string
ncache *cache.Cache ncache *cache.Cache
ncap int ncap int
@ -177,11 +178,11 @@ func (w *ResponseWriter) WriteMsg(res *dns.Msg) error {
if hasKey && duration > 0 { if hasKey && duration > 0 {
if w.state.Match(res) { if w.state.Match(res) {
w.set(res, key, mt, duration) w.set(res, key, mt, duration)
cacheSize.WithLabelValues(w.server, Success, w.zonesMetricLabel).Set(float64(w.pcache.Len())) cacheSize.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.pcache.Len()))
cacheSize.WithLabelValues(w.server, Denial, w.zonesMetricLabel).Set(float64(w.ncache.Len())) cacheSize.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.ncache.Len()))
} else { } else {
// Don't log it, but increment counter // Don't log it, but increment counter
cacheDrops.WithLabelValues(w.server, w.zonesMetricLabel).Inc() cacheDrops.WithLabelValues(w.server, w.zonesMetricLabel, w.viewMetricLabel).Inc()
} }
} }
@ -219,7 +220,7 @@ func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration
i.wildcard = w.wildcardFunc() i.wildcard = w.wildcardFunc()
} }
if w.pcache.Add(key, i) { if w.pcache.Add(key, i) {
evictions.WithLabelValues(w.server, Success, w.zonesMetricLabel).Inc() evictions.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Inc()
} }
// when pre-fetching, remove the negative cache entry if it exists // when pre-fetching, remove the negative cache entry if it exists
if w.prefetch { if w.prefetch {
@ -236,7 +237,7 @@ func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration
i.wildcard = w.wildcardFunc() i.wildcard = w.wildcardFunc()
} }
if w.ncache.Add(key, i) { if w.ncache.Add(key, i) {
evictions.WithLabelValues(w.server, Denial, w.zonesMetricLabel).Inc() evictions.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Inc()
} }
case response.OtherError: case response.OtherError:

View file

@ -60,7 +60,7 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
cw := newPrefetchResponseWriter(server, state, c) cw := newPrefetchResponseWriter(server, state, c)
go c.doPrefetch(ctx, state, cw, i, now) go c.doPrefetch(ctx, state, cw, i, now)
} }
servedStale.WithLabelValues(server, c.zonesMetricLabel).Inc() servedStale.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
} else if c.shouldPrefetch(i, now) { } else if c.shouldPrefetch(i, now) {
cw := newPrefetchResponseWriter(server, state, c) cw := newPrefetchResponseWriter(server, state, c)
go c.doPrefetch(ctx, state, cw, i, now) go c.doPrefetch(ctx, state, cw, i, now)
@ -89,7 +89,7 @@ func wildcardFunc(ctx context.Context) func() string {
} }
func (c *Cache) doPrefetch(ctx context.Context, state request.Request, cw *ResponseWriter, i *item, now time.Time) { func (c *Cache) doPrefetch(ctx context.Context, state request.Request, cw *ResponseWriter, i *item, now time.Time) {
cachePrefetches.WithLabelValues(cw.server, c.zonesMetricLabel).Inc() cachePrefetches.WithLabelValues(cw.server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
c.doRefresh(ctx, state, cw) c.doRefresh(ctx, state, cw)
// When prefetching we loose the item i, and with it the frequency // When prefetching we loose the item i, and with it the frequency
@ -122,13 +122,13 @@ func (c *Cache) Name() string { return "cache" }
// getIgnoreTTL unconditionally returns an item if it exists in the cache. // getIgnoreTTL unconditionally returns an item if it exists in the cache.
func (c *Cache) getIgnoreTTL(now time.Time, state request.Request, server string) *item { func (c *Cache) getIgnoreTTL(now time.Time, state request.Request, server string) *item {
k := hash(state.Name(), state.QType()) k := hash(state.Name(), state.QType())
cacheRequests.WithLabelValues(server, c.zonesMetricLabel).Inc() cacheRequests.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
if i, ok := c.ncache.Get(k); ok { if i, ok := c.ncache.Get(k); ok {
itm := i.(*item) itm := i.(*item)
ttl := itm.ttl(now) ttl := itm.ttl(now)
if itm.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) { if itm.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) {
cacheHits.WithLabelValues(server, Denial, c.zonesMetricLabel).Inc() cacheHits.WithLabelValues(server, Denial, c.zonesMetricLabel, c.viewMetricLabel).Inc()
return i.(*item) return i.(*item)
} }
} }
@ -136,11 +136,11 @@ func (c *Cache) getIgnoreTTL(now time.Time, state request.Request, server string
itm := i.(*item) itm := i.(*item)
ttl := itm.ttl(now) ttl := itm.ttl(now)
if itm.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) { if itm.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) {
cacheHits.WithLabelValues(server, Success, c.zonesMetricLabel).Inc() cacheHits.WithLabelValues(server, Success, c.zonesMetricLabel, c.viewMetricLabel).Inc()
return i.(*item) return i.(*item)
} }
} }
cacheMisses.WithLabelValues(server, c.zonesMetricLabel).Inc() cacheMisses.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
return nil return nil
} }

View file

@ -14,54 +14,54 @@ var (
Subsystem: "cache", Subsystem: "cache",
Name: "entries", Name: "entries",
Help: "The number of elements in the cache.", Help: "The number of elements in the cache.",
}, []string{"server", "type", "zones"}) }, []string{"server", "type", "zones", "view"})
// cacheRequests is a counter of all requests through the cache. // cacheRequests is a counter of all requests through the cache.
cacheRequests = promauto.NewCounterVec(prometheus.CounterOpts{ cacheRequests = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: "cache", Subsystem: "cache",
Name: "requests_total", Name: "requests_total",
Help: "The count of cache requests.", Help: "The count of cache requests.",
}, []string{"server", "zones"}) }, []string{"server", "zones", "view"})
// cacheHits is counter of cache hits by cache type. // cacheHits is counter of cache hits by cache type.
cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{ cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: "cache", Subsystem: "cache",
Name: "hits_total", Name: "hits_total",
Help: "The count of cache hits.", Help: "The count of cache hits.",
}, []string{"server", "type", "zones"}) }, []string{"server", "type", "zones", "view"})
// cacheMisses is the counter of cache misses. - Deprecated // cacheMisses is the counter of cache misses. - Deprecated
cacheMisses = promauto.NewCounterVec(prometheus.CounterOpts{ cacheMisses = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: "cache", Subsystem: "cache",
Name: "misses_total", Name: "misses_total",
Help: "The count of cache misses. Deprecated, derive misses from cache hits/requests counters.", Help: "The count of cache misses. Deprecated, derive misses from cache hits/requests counters.",
}, []string{"server", "zones"}) }, []string{"server", "zones", "view"})
// cachePrefetches is the number of time the cache has prefetched a cached item. // cachePrefetches is the number of time the cache has prefetched a cached item.
cachePrefetches = promauto.NewCounterVec(prometheus.CounterOpts{ cachePrefetches = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: "cache", Subsystem: "cache",
Name: "prefetch_total", Name: "prefetch_total",
Help: "The number of times the cache has prefetched a cached item.", Help: "The number of times the cache has prefetched a cached item.",
}, []string{"server", "zones"}) }, []string{"server", "zones", "view"})
// cacheDrops is the number responses that are not cached, because the reply is malformed. // cacheDrops is the number responses that are not cached, because the reply is malformed.
cacheDrops = promauto.NewCounterVec(prometheus.CounterOpts{ cacheDrops = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: "cache", Subsystem: "cache",
Name: "drops_total", Name: "drops_total",
Help: "The number responses that are not cached, because the reply is malformed.", Help: "The number responses that are not cached, because the reply is malformed.",
}, []string{"server", "zones"}) }, []string{"server", "zones", "view"})
// servedStale is the number of requests served from stale cache entries. // servedStale is the number of requests served from stale cache entries.
servedStale = promauto.NewCounterVec(prometheus.CounterOpts{ servedStale = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: "cache", Subsystem: "cache",
Name: "served_stale_total", Name: "served_stale_total",
Help: "The number of requests served from stale cache entries.", Help: "The number of requests served from stale cache entries.",
}, []string{"server", "zones"}) }, []string{"server", "zones", "view"})
// evictions is the counter of cache evictions. // evictions is the counter of cache evictions.
evictions = promauto.NewCounterVec(prometheus.CounterOpts{ evictions = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: "cache", Subsystem: "cache",
Name: "evictions_total", Name: "evictions_total",
Help: "The count of cache evictions.", Help: "The count of cache evictions.",
}, []string{"server", "type", "zones"}) }, []string{"server", "type", "zones", "view"})
) )

View file

@ -23,6 +23,12 @@ func setup(c *caddy.Controller) error {
if err != nil { if err != nil {
return plugin.Error("cache", err) return plugin.Error("cache", err)
} }
c.OnStartup(func() error {
ca.viewMetricLabel = dnsserver.GetConfig(c).ViewName
return nil
})
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
ca.Next = next ca.Next = next
return ca return ca

View file

@ -27,17 +27,18 @@ func ContextWithMetadata(ctx context.Context) context.Context {
// ServeDNS implements the plugin.Handler interface. // ServeDNS implements the plugin.Handler interface.
func (m *Metadata) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { func (m *Metadata) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
ctx = ContextWithMetadata(ctx) rcode, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, w, r)
return rcode, err
}
state := request.Request{W: w, Req: r} // Collect will retrieve metadata functions from each metadata provider and update the context
func (m *Metadata) Collect(ctx context.Context, state request.Request) context.Context {
ctx = ContextWithMetadata(ctx)
if plugin.Zones(m.Zones).Matches(state.Name()) != "" { if plugin.Zones(m.Zones).Matches(state.Name()) != "" {
// Go through all Providers and collect metadata. // Go through all Providers and collect metadata.
for _, p := range m.Providers { for _, p := range m.Providers {
ctx = p.Metadata(ctx, state) ctx = p.Metadata(ctx, state)
} }
} }
return ctx
rcode, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, w, r)
return rcode, err
} }

View file

@ -47,7 +47,10 @@ func TestMetadataServeDNS(t *testing.T) {
} }
ctx := context.TODO() ctx := context.TODO()
m.ServeDNS(ctx, &test.ResponseWriter{}, new(dns.Msg)) w := &test.ResponseWriter{}
r := new(dns.Msg)
ctx = m.Collect(ctx, request.Request{W: w, Req: r})
m.ServeDNS(ctx, w, r)
nctx := next.ctx nctx := next.ctx
for _, expected := range expectedMetadata { for _, expected := range expectedMetadata {

View file

@ -14,12 +14,12 @@ the following metrics are exported:
* `coredns_build_info{version, revision, goversion}` - info about CoreDNS itself. * `coredns_build_info{version, revision, goversion}` - info about CoreDNS itself.
* `coredns_panics_total{}` - total number of panics. * `coredns_panics_total{}` - total number of panics.
* `coredns_dns_requests_total{server, zone, proto, family, type}` - total query count. * `coredns_dns_requests_total{server, zone, view, proto, family, type}` - total query count.
* `coredns_dns_request_duration_seconds{server, zone, type}` - duration to process each query. * `coredns_dns_request_duration_seconds{server, zone, view, type}` - duration to process each query.
* `coredns_dns_request_size_bytes{server, zone, proto}` - size of the request in bytes. * `coredns_dns_request_size_bytes{server, zone, view, proto}` - size of the request in bytes.
* `coredns_dns_do_requests_total{server, zone}` - queries that have the DO bit set * `coredns_dns_do_requests_total{server, view, zone}` - queries that have the DO bit set
* `coredns_dns_response_size_bytes{server, zone, proto}` - response size in bytes. * `coredns_dns_response_size_bytes{server, zone, view, proto}` - response size in bytes.
* `coredns_dns_responses_total{server, zone, rcode, plugin}` - response per zone, rcode and plugin. * `coredns_dns_responses_total{server, zone, view, rcode, plugin}` - response per zone, rcode and plugin.
* `coredns_dns_https_responses_total{server, status}` - responses per server and http status code. * `coredns_dns_https_responses_total{server, status}` - responses per server and http status code.
* `coredns_plugin_enabled{server, zone, name}` - indicates whether a plugin is enabled on per server and zone basis. * `coredns_plugin_enabled{server, zone, name}` - indicates whether a plugin is enabled on per server and zone basis.

View file

@ -22,3 +22,16 @@ func WithServer(ctx context.Context) string {
} }
return srv.(*dnsserver.Server).Addr return srv.(*dnsserver.Server).Addr
} }
// WithView returns the name of the view currently handling the request, if a view is defined.
//
// Basic usage with a metric:
//
// <metric>.WithLabelValues(metrics.WithView(ctx), labels..).Add(1)
func WithView(ctx context.Context) string {
v := ctx.Value(dnsserver.ViewKey{})
if v == nil {
return ""
}
return v.(string)
}

View file

@ -34,7 +34,7 @@ func (m *Metrics) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
rc = status rc = status
} }
plugin := m.authoritativePlugin(rw.Caller) plugin := m.authoritativePlugin(rw.Caller)
vars.Report(WithServer(ctx), state, zone, rcode.ToString(rc), plugin, rw.Len, rw.Start) vars.Report(WithServer(ctx), state, zone, WithView(ctx), rcode.ToString(rc), plugin, rw.Len, rw.Start)
return status, err return status, err
} }

View file

@ -9,7 +9,7 @@ import (
// Report reports the metrics data associated with request. This function is exported because it is also // Report reports the metrics data associated with request. This function is exported because it is also
// called from core/dnsserver to report requests hitting the server that should not be handled and are thus // called from core/dnsserver to report requests hitting the server that should not be handled and are thus
// not sent down the plugin chain. // not sent down the plugin chain.
func Report(server string, req request.Request, zone, rcode, plugin string, size int, start time.Time) { func Report(server string, req request.Request, zone, view, rcode, plugin string, size int, start time.Time) {
// Proto and Family. // Proto and Family.
net := req.Proto() net := req.Proto()
fam := "1" fam := "1"
@ -18,16 +18,16 @@ func Report(server string, req request.Request, zone, rcode, plugin string, size
} }
if req.Do() { if req.Do() {
RequestDo.WithLabelValues(server, zone).Inc() RequestDo.WithLabelValues(server, zone, view).Inc()
} }
qType := qTypeString(req.QType()) qType := qTypeString(req.QType())
RequestCount.WithLabelValues(server, zone, net, fam, qType).Inc() RequestCount.WithLabelValues(server, zone, view, net, fam, qType).Inc()
RequestDuration.WithLabelValues(server, zone).Observe(time.Since(start).Seconds()) RequestDuration.WithLabelValues(server, zone, view).Observe(time.Since(start).Seconds())
ResponseSize.WithLabelValues(server, zone, net).Observe(float64(size)) ResponseSize.WithLabelValues(server, zone, view, net).Observe(float64(size))
RequestSize.WithLabelValues(server, zone, net).Observe(float64(req.Len())) RequestSize.WithLabelValues(server, zone, view, net).Observe(float64(req.Len()))
ResponseRcode.WithLabelValues(server, zone, rcode, plugin).Inc() ResponseRcode.WithLabelValues(server, zone, view, rcode, plugin).Inc()
} }

View file

@ -14,7 +14,7 @@ var (
Subsystem: subsystem, Subsystem: subsystem,
Name: "requests_total", Name: "requests_total",
Help: "Counter of DNS requests made per zone, protocol and family.", Help: "Counter of DNS requests made per zone, protocol and family.",
}, []string{"server", "zone", "proto", "family", "type"}) }, []string{"server", "zone", "view", "proto", "family", "type"})
RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
@ -22,7 +22,7 @@ var (
Name: "request_duration_seconds", Name: "request_duration_seconds",
Buckets: plugin.TimeBuckets, Buckets: plugin.TimeBuckets,
Help: "Histogram of the time (in seconds) each request took per zone.", Help: "Histogram of the time (in seconds) each request took per zone.",
}, []string{"server", "zone"}) }, []string{"server", "zone", "view"})
RequestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ RequestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
@ -30,14 +30,14 @@ var (
Name: "request_size_bytes", Name: "request_size_bytes",
Help: "Size of the EDNS0 UDP buffer in bytes (64K for TCP) per zone and protocol.", Help: "Size of the EDNS0 UDP buffer in bytes (64K for TCP) per zone and protocol.",
Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3}, Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3},
}, []string{"server", "zone", "proto"}) }, []string{"server", "zone", "view", "proto"})
RequestDo = promauto.NewCounterVec(prometheus.CounterOpts{ RequestDo = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: subsystem, Subsystem: subsystem,
Name: "do_requests_total", Name: "do_requests_total",
Help: "Counter of DNS requests with DO bit set per zone.", Help: "Counter of DNS requests with DO bit set per zone.",
}, []string{"server", "zone"}) }, []string{"server", "zone", "view"})
ResponseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ ResponseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
@ -45,14 +45,14 @@ var (
Name: "response_size_bytes", Name: "response_size_bytes",
Help: "Size of the returned response in bytes.", Help: "Size of the returned response in bytes.",
Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3}, Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3},
}, []string{"server", "zone", "proto"}) }, []string{"server", "zone", "view", "proto"})
ResponseRcode = promauto.NewCounterVec(prometheus.CounterOpts{ ResponseRcode = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,
Subsystem: subsystem, Subsystem: subsystem,
Name: "responses_total", Name: "responses_total",
Help: "Counter of response status codes.", Help: "Counter of response status codes.",
}, []string{"server", "zone", "rcode", "plugin"}) }, []string{"server", "zone", "view", "rcode", "plugin"})
Panic = promauto.NewCounter(prometheus.CounterOpts{ Panic = promauto.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace, Namespace: plugin.Namespace,

View file

@ -0,0 +1,47 @@
package expression
import (
"context"
"errors"
"net"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/request"
)
// DefaultEnv returns the default set of custom state variables and functions available to for use in expression evaluation.
func DefaultEnv(ctx context.Context, state *request.Request) map[string]interface{} {
return map[string]interface{}{
"incidr": func(ipStr, cidrStr string) (bool, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return false, errors.New("first argument is not an IP address")
}
_, cidr, err := net.ParseCIDR(cidrStr)
if err != nil {
return false, err
}
return cidr.Contains(ip), nil
},
"metadata": func(label string) string {
f := metadata.ValueFunc(ctx, label)
if f == nil {
return ""
}
return f()
},
"type": state.Type,
"name": state.Name,
"class": state.Class,
"proto": state.Proto,
"size": state.Len,
"client_ip": state.IP,
"port": state.Port,
"id": func() int { return int(state.Req.Id) },
"opcode": func() int { return state.Req.Opcode },
"do": state.Do,
"bufsize": state.Size,
"server_ip": state.LocalIP,
"server_port": state.LocalPort,
}
}

View file

@ -0,0 +1,73 @@
package expression
import (
"context"
"testing"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/request"
)
func TestInCidr(t *testing.T) {
incidr := DefaultEnv(context.Background(), &request.Request{})["incidr"]
cases := []struct {
ip string
cidr string
expected bool
shouldErr bool
}{
// positive
{ip: "1.2.3.4", cidr: "1.2.0.0/16", expected: true, shouldErr: false},
{ip: "10.2.3.4", cidr: "1.2.0.0/16", expected: false, shouldErr: false},
{ip: "1:2::3:4", cidr: "1:2::/64", expected: true, shouldErr: false},
{ip: "A:2::3:4", cidr: "1:2::/64", expected: false, shouldErr: false},
// negative
{ip: "1.2.3.4", cidr: "invalid", shouldErr: true},
{ip: "invalid", cidr: "1.2.0.0/16", shouldErr: true},
}
for i, c := range cases {
r, err := incidr.(func(string, string) (bool, error))(c.ip, c.cidr)
if err != nil && !c.shouldErr {
t.Errorf("Test %d: unexpected error %v", i, err)
continue
}
if err == nil && c.shouldErr {
t.Errorf("Test %d: expected error", i)
continue
}
if c.shouldErr {
continue
}
if r != c.expected {
t.Errorf("Test %d: expected %v", i, c.expected)
continue
}
}
}
func TestMetadata(t *testing.T) {
ctx := metadata.ContextWithMetadata(context.Background())
metadata.SetValueFunc(ctx, "test/metadata", func() string {
return "success"
})
f := DefaultEnv(ctx, &request.Request{})["metadata"]
cases := []struct {
label string
expected string
shouldErr bool
}{
{label: "test/metadata", expected: "success"},
{label: "test/nonexistent", expected: ""},
}
for i, c := range cases {
r := f.(func(string) string)(c.label)
if r != c.expected {
t.Errorf("Test %d: expected %v", i, c.expected)
continue
}
}
}

View file

@ -340,13 +340,12 @@ func TestMetadataReplacement(t *testing.T) {
Next: next, Next: next,
} }
m.ServeDNS(context.TODO(), &test.ResponseWriter{}, new(dns.Msg))
ctx := next.ctx // important because the m.ServeDNS has only now populated the context
w := dnstest.NewRecorder(&test.ResponseWriter{}) w := dnstest.NewRecorder(&test.ResponseWriter{})
r := new(dns.Msg) r := new(dns.Msg)
r.SetQuestion("example.org.", dns.TypeHINFO) r.SetQuestion("example.org.", dns.TypeHINFO)
ctx := m.Collect(context.TODO(), request.Request{W: w, Req: r})
repl := New() repl := New()
state := request.Request{W: w, Req: r} state := request.Request{W: w, Req: r}

View file

@ -604,8 +604,8 @@ func TestRewriteEDNS0LocalVariable(t *testing.T) {
} }
rw.Rules = []Rule{r} rw.Rules = []Rule{r}
ctx := context.TODO()
rec := dnstest.NewRecorder(&test.ResponseWriter{}) rec := dnstest.NewRecorder(&test.ResponseWriter{})
ctx := meta.Collect(context.TODO(), request.Request{W: rec, Req: m})
meta.ServeDNS(ctx, rec, m) meta.ServeDNS(ctx, rec, m)
resp := rec.Msg resp := rec.Msg

135
plugin/view/README.md Normal file
View file

@ -0,0 +1,135 @@
# view
## Name
*view* - defines conditions that must be met for a DNS request to be routed to the server block.
## Description
*view* defines an expression that must evaluate to true for a DNS request to be routed to the server block.
This enables advanced server block routing functions such as split dns.
## Syntax
```
view NAME {
expr EXPRESSION
}
```
* `view` **NAME** - The name of the view used by metrics and exported as metadata for requests that match the
view's expression
* `expr` **EXPRESSION** - CoreDNS will only route incoming queries to the enclosing server block
if the **EXPRESSION** evaluates to true. See the **Expressions** section for available variables and functions.
If multiple instances of view are defined, all **EXPRESSION** must evaluate to true for CoreDNS will only route
incoming queries to the enclosing server block.
For expression syntax and examples, see the Expressions and Examples sections.
## Examples
Implement CIDR based split DNS routing. This will return a different
answer for `test.` depending on client's IP address. It returns ...
* `test. 3600 IN A 1.1.1.1`, for queries with a source address in 127.0.0.0/24
* `test. 3600 IN A 2.2.2.2`, for queries with a source address in 192.168.0.0/16
* `test. 3600 IN A 3.3.3.3`, for all others
```
. {
view example1 {
expr incidr(client_ip(), '127.0.0.0/24')
}
hosts {
1.1.1.1 test
}
}
. {
view example2 {
expr incidr(client_ip(), '192.168.0.0/16')
}
hosts {
2.2.2.2 test
}
}
. {
hosts {
3.3.3.3 test
}
}
```
Send all `A` and `AAAA` requests to `10.0.0.6`, and all other requests to `10.0.0.1`.
```
. {
view example {
expr type() in ['A', 'AAAA']
}
forward . 10.0.0.6
}
. {
forward . 10.0.0.1
}
```
Send all requests for `abc.*.example.com` (where * can be any number of labels), to `10.0.0.2`, and all other
requests to `10.0.0.1`.
Note that the regex pattern is enclosed in single quotes, and backslashes are escaped with backslashes.
```
. {
view example {
expr name() matches '^abc\\..*\\.example\\.com\\.$'
}
forward . 10.0.0.2
}
. {
forward . 10.0.0.1
}
```
## Expressions
To evaluate expressions, *view* uses the antonmedv/expr package (https://github.com/antonmedv/expr).
For example, an expression could look like:
`(type() == 'A' && name() == 'example.com') || client_ip() == '1.2.3.4'`.
All expressions should be written to evaluate to a boolean value.
See https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md as a detailed reference for valid syntax.
### Available Expression Functions
In the context of the *view* plugin, expressions can reference DNS query information by using utility
functions defined below.
#### DNS Query Functions
* `bufsize() int`: the EDNS0 buffer size advertised in the query
* `class() string`: class of the request (IN, CH, ...)
* `client_ip() string`: client's IP address, for IPv6 addresses these are enclosed in brackets: `[::1]`
* `do() bool`: the EDNS0 DO (DNSSEC OK) bit set in the query
* `id() int`: query ID
* `name() string`: name of the request (the domain name requested)
* `opcode() int`: query OPCODE
* `port() string`: client's port
* `proto() string`: protocol used (tcp or udp)
* `server_ip() string`: server's IP address; for IPv6 addresses these are enclosed in brackets: `[::1]`
* `server_port() string` : client's port
* `size() int`: request size in bytes
* `type() string`: type of the request (A, AAAA, TXT, ...)
#### Utility Functions
* `incidr(ip string, cidr string) bool`: returns true if _ip_ is within _cidr_
* `metadata(label string)` - returns the value for the metadata matching _label_
## Metadata
The view plugin will publish the following metadata, if the *metadata*
plugin is also enabled:
* `view/name`: the name of the view handling the current request

16
plugin/view/metadata.go Normal file
View file

@ -0,0 +1,16 @@
package view
import (
"context"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/request"
)
// Metadata implements the metadata.Provider interface.
func (v *View) Metadata(ctx context.Context, state request.Request) context.Context {
metadata.SetValueFunc(ctx, "view/name", func() string {
return v.viewName
})
return ctx
}

65
plugin/view/setup.go Normal file
View file

@ -0,0 +1,65 @@
package view
import (
"context"
"strings"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/expression"
"github.com/antonmedv/expr"
)
func init() { plugin.Register("view", setup) }
func setup(c *caddy.Controller) error {
cond, err := parse(c)
if err != nil {
return plugin.Error("view", err)
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
cond.Next = next
return cond
})
return nil
}
func parse(c *caddy.Controller) (*View, error) {
v := new(View)
i := 0
for c.Next() {
i++
if i > 1 {
return nil, plugin.ErrOnce
}
args := c.RemainingArgs()
if len(args) != 1 {
return nil, c.ArgErr()
}
v.viewName = args[0]
for c.NextBlock() {
switch c.Val() {
case "expr":
args := c.RemainingArgs()
prog, err := expr.Compile(strings.Join(args, " "), expr.Env(expression.DefaultEnv(context.Background(), nil)))
if err != nil {
return v, err
}
v.progs = append(v.progs, prog)
if err != nil {
return nil, err
}
continue
default:
return nil, c.Errf("unknown property '%s'", c.Val())
}
}
}
return v, nil
}

38
plugin/view/setup_test.go Normal file
View file

@ -0,0 +1,38 @@
package view
import (
"testing"
"github.com/coredns/caddy"
)
func TestSetup(t *testing.T) {
tests := []struct {
input string
shouldErr bool
progCount int
}{
{"view example {\n expr name() == 'example.com.'\n}", false, 1},
{"view example {\n expr incidr(client_ip(), '10.0.0.0/24')\n}", false, 1},
{"view example {\n expr name() == 'example.com.'\n expr name() == 'example2.com.'\n}", false, 2},
{"view", true, 0},
{"view example {\n expr invalid expression\n}", true, 0},
}
for i, test := range tests {
v, err := parse(caddy.NewTestController("dns", test.input))
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error but found none for input %s", i, test.input)
}
if err != nil && !test.shouldErr {
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
}
if test.shouldErr {
continue
}
if test.progCount != len(v.progs) {
t.Errorf("Test %d: Expected prog length %d, but got %d for %s.", i, test.progCount, len(v.progs), test.input)
}
}
}

48
plugin/view/view.go Normal file
View file

@ -0,0 +1,48 @@
package view
import (
"context"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/expression"
"github.com/coredns/coredns/request"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
"github.com/miekg/dns"
)
// View is a plugin that enables configuring expression based advanced routing
type View struct {
progs []*vm.Program
viewName string
Next plugin.Handler
}
// Filter implements dnsserver.Viewer. It returns true if all View rules evaluate to true for the given state.
func (v *View) Filter(ctx context.Context, state *request.Request) bool {
env := expression.DefaultEnv(ctx, state)
for _, prog := range v.progs {
result, err := expr.Run(prog, env)
if err != nil {
return false
}
if b, ok := result.(bool); ok && b {
continue
}
// anything other than a boolean true result is considered false
return false
}
return true
}
// ViewName implements dnsserver.Viewer. It returns the view name
func (v *View) ViewName() string { return v.viewName }
// Name implements the Handler interface
func (*View) Name() string { return "view" }
// ServeDNS implements the Handler interface.
func (v *View) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
return plugin.NextOrFailure(v.Name(), v.Next, ctx, w, r)
}

163
test/view_test.go Normal file
View file

@ -0,0 +1,163 @@
package test
import (
"strings"
"testing"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
func TestView(t *testing.T) {
// Hack to get an available port - We spin up a temporary dummy coredns on :0 to get the port number, then we re-use
// that one port consistently across all server blocks.
corefile := `example.org:0 {
erratic
}`
tmp, addr, _, err := CoreDNSServerAndPorts(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
port := addr[strings.LastIndex(addr, ":")+1:]
// Corefile with test views
corefile = `
# split-type config: splits quries for A/AAAA into separate views
split-type:` + port + ` {
view test-view-a {
expr type() == 'A'
}
hosts {
1.2.3.4 test.split-type
}
}
split-type:` + port + ` {
view test-view-aaaa {
expr type() == 'AAAA'
}
hosts {
1:2:3::4 test.split-type
}
}
# split-name config: splits queries into separate views based on first label in query name ("one", "two")
split-name:` + port + ` {
view test-view-1 {
expr name() matches '^one\\..*\\.split-name\\.$'
}
hosts {
1.1.1.1 one.test.split-name one.test.test.test.split-name
}
}
split-name:` + port + ` {
view test-view-2 {
expr name() matches '^two\\..*\\.split-name\\.$'
}
hosts {
2.2.2.2 two.test.split-name two.test.test.test.split-name
}
}
split-name:` + port + ` {
hosts {
3.3.3.3 default.test.split-name
}
}
# metadata config: verifies that metadata is properly collected by the server,
# and that metadata function correctly looks up the value of the metadata.
metadata:` + port + ` {
metadata
view test-view-meta1 {
# This is never true
expr metadata('view/name') == 'not-the-view-name'
}
hosts {
1.1.1.1 test.metadata
}
}
metadata:` + port + ` {
view test-view-meta2 {
# This is never true. The metadata plugin is not enabled in this server block so the metadata function returns
# an empty string
expr metadata('view/name') == 'test-view-meta2'
}
hosts {
2.2.2.2 test.metadata
}
}
metadata:` + port + ` {
metadata
view test-view-meta3 {
# This is always true. Queries in the zone 'metadata.' should always be served using this view.
expr metadata('view/name') == 'test-view-meta3'
}
hosts {
2.2.2.2 test.metadata
}
}
metadata:` + port + ` {
# This block should never be reached since the prior view in the same zone is always true
hosts {
3.3.3.3 test.metadata
}
}
`
i, addr, _, err := CoreDNSServerAndPorts(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
// there are multiple sever blocks, but they are all on the same port, so it's a single server instance to stop
defer i.Stop()
// stop the temporary instance before starting tests.
tmp.Stop()
viewTest(t, "split-type A", addr, "test.split-type.", dns.TypeA, dns.RcodeSuccess,
[]dns.RR{test.A("test.split-type. 303 IN A 1.2.3.4")})
viewTest(t, "split-type AAAA", addr, "test.split-type.", dns.TypeAAAA, dns.RcodeSuccess,
[]dns.RR{test.AAAA("test.split-type. 303 IN AAAA 1:2:3::4")})
viewTest(t, "split-name one.test.test.test.split-name", addr, "one.test.test.test.split-name.", dns.TypeA, dns.RcodeSuccess,
[]dns.RR{test.A("one.test.test.test.split-name. 303 IN A 1.1.1.1")})
viewTest(t, "split-name one.test.split-name", addr, "one.test.split-name.", dns.TypeA, dns.RcodeSuccess,
[]dns.RR{test.A("one.test.split-name. 303 IN A 1.1.1.1")})
viewTest(t, "split-name two.test.test.test.split-name", addr, "two.test.test.test.split-name.", dns.TypeA, dns.RcodeSuccess,
[]dns.RR{test.A("two.test.test.test.split-name. 303 IN A 2.2.2.2")})
viewTest(t, "split-name two.test.split-name", addr, "two.test.split-name.", dns.TypeA, dns.RcodeSuccess,
[]dns.RR{test.A("two.test.split-name. 303 IN A 2.2.2.2")})
viewTest(t, "split-name default.test.split-name", addr, "default.test.split-name.", dns.TypeA, dns.RcodeSuccess,
[]dns.RR{test.A("default.test.split-name. 303 IN A 3.3.3.3")})
viewTest(t, "metadata test.metadata", addr, "test.metadata.", dns.TypeA, dns.RcodeSuccess,
[]dns.RR{test.A("test.metadata. 303 IN A 2.2.2.2")})
}
func viewTest(t *testing.T, testName, addr, qname string, qtype uint16, expectRcode int, expectAnswers []dns.RR) {
t.Run(testName, func(t *testing.T) {
m := new(dns.Msg)
m.SetQuestion(qname, qtype)
resp, err := dns.Exchange(m, addr)
if err != nil {
t.Fatalf("Expected to receive reply, but didn't: %s", err)
}
tc := test.Case{
Qname: qname, Qtype: qtype,
Rcode: expectRcode,
Answer: expectAnswers,
}
err = test.SortAndCheck(resp, tc)
if err != nil {
t.Error(err)
}
})
}