From b56b080a7c1125cd97c0a2edd7ae21bc1bdcd2a5 Mon Sep 17 00:00:00 2001 From: Chris O'Haver Date: Thu, 8 Sep 2022 14:56:27 -0400 Subject: [PATCH] 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 --- core/dnsserver/address.go | 23 +++- core/dnsserver/config.go | 16 +++ core/dnsserver/onstartup.go | 2 +- core/dnsserver/register.go | 38 ++++-- core/dnsserver/server.go | 122 ++++++++++++----- core/dnsserver/server_grpc.go | 8 +- core/dnsserver/server_https.go | 14 +- core/dnsserver/server_tls.go | 8 +- core/dnsserver/view.go | 20 +++ core/dnsserver/zdirectives.go | 1 + core/plugin/zplugin.go | 1 + go.mod | 1 + go.sum | 15 +++ plugin.cfg | 1 + plugin/cache/README.md | 16 +-- plugin/cache/cache.go | 11 +- plugin/cache/handler.go | 12 +- plugin/cache/metrics.go | 16 +-- plugin/cache/setup.go | 6 + plugin/metadata/metadata.go | 13 +- plugin/metadata/metadata_test.go | 5 +- plugin/metrics/README.md | 12 +- plugin/metrics/context.go | 13 ++ plugin/metrics/handler.go | 2 +- plugin/metrics/vars/report.go | 14 +- plugin/metrics/vars/vars.go | 12 +- plugin/pkg/expression/expression.go | 47 +++++++ plugin/pkg/expression/expression_test.go | 73 ++++++++++ plugin/pkg/replacer/replacer_test.go | 5 +- plugin/rewrite/rewrite_test.go | 2 +- plugin/view/README.md | 135 +++++++++++++++++++ plugin/view/metadata.go | 16 +++ plugin/view/setup.go | 65 +++++++++ plugin/view/setup_test.go | 38 ++++++ plugin/view/view.go | 48 +++++++ test/view_test.go | 163 +++++++++++++++++++++++ 36 files changed, 880 insertions(+), 114 deletions(-) create mode 100644 core/dnsserver/view.go create mode 100644 plugin/pkg/expression/expression.go create mode 100644 plugin/pkg/expression/expression_test.go create mode 100644 plugin/view/README.md create mode 100644 plugin/view/metadata.go create mode 100644 plugin/view/setup.go create mode 100644 plugin/view/setup_test.go create mode 100644 plugin/view/view.go create mode 100644 test/view_test.go diff --git a/core/dnsserver/address.go b/core/dnsserver/address.go index 547cd35ec..872e44cb7 100644 --- a/core/dnsserver/address.go +++ b/core/dnsserver/address.go @@ -49,11 +49,23 @@ func newOverlapZone() *zoneOverlap { // 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 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 { // exact same zone already registered 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 z.Address == "" { // 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 } } - // there is no overlap, keep the current zoneAddr for future checks - zo.registeredAddr[z] = z - zo.unboundOverlap[uz] = z + // there is no overlap 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} +} diff --git a/core/dnsserver/config.go b/core/dnsserver/config.go index c34398b39..3da86271e 100644 --- a/core/dnsserver/config.go +++ b/core/dnsserver/config.go @@ -1,12 +1,14 @@ package dnsserver import ( + "context" "crypto/tls" "fmt" "net/http" "github.com/coredns/caddy" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" ) // Config configuration for a single server. @@ -40,6 +42,14 @@ type Config struct { // may depend on it. 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 *tls.Config @@ -60,8 +70,14 @@ type Config struct { // 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. 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 func keyForConfig(blocIndex int, blocKeyIndex int) string { return fmt.Sprintf("%d:%d", blocIndex, blocKeyIndex) diff --git a/core/dnsserver/onstartup.go b/core/dnsserver/onstartup.go index b016f1ee5..572b3589f 100644 --- a/core/dnsserver/onstartup.go +++ b/core/dnsserver/onstartup.go @@ -21,7 +21,7 @@ func checkZoneSyntax(zone string) bool { // startUpZones creates the text that we show when starting up: // grpc://example.com.:1055 // 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 := "" keys := make([]string, len(zones)) diff --git a/core/dnsserver/register.go b/core/dnsserver/register.go index d64a3e7d0..e94accc22 100644 --- a/core/dnsserver/register.go +++ b/core/dnsserver/register.go @@ -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. 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 // to all other config in the same block . Doing this results in zones // 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 } @@ -253,7 +267,15 @@ func (h *dnsContext) validateZonesAndListeningAddresses() error { for _, h := range conf.ListenHosts { // Validate the overlapping of ZoneAddr 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 { return fmt.Errorf("cannot serve %s - it is already defined", akey.String()) } diff --git a/core/dnsserver/server.go b/core/dnsserver/server.go index 15d1f90ce..478287bf8 100644 --- a/core/dnsserver/server.go +++ b/core/dnsserver/server.go @@ -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. m sync.Mutex // protects the servers - zones map[string]*Config // zones keyed by their address - dnsWg sync.WaitGroup // used to wait on outstanding connections - graceTimeout time.Duration // the maximum duration of a graceful shutdown - trace trace.Trace // the trace plugin for the server - debug bool // disable recover() - stacktrace bool // enable stacktrace in recover error log - classChaos bool // allow non-INET class queries + zones map[string][]*Config // zones keyed by their address + dnsWg sync.WaitGroup // used to wait on outstanding connections + graceTimeout time.Duration // the maximum duration of a graceful shutdown + trace trace.Trace // the trace plugin for the server + debug bool // disable recover() + stacktrace bool // enable stacktrace in recover error log + classChaos bool // allow non-INET class queries 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 // queries are blocked unless queries from enableChaos are loaded. func NewServer(addr string, group []*Config) (*Server, error) { s := &Server{ Addr: addr, - zones: make(map[string]*Config), + zones: make(map[string][]*Config), graceTimeout: 5 * time.Second, tsigSecret: make(map[string]string), } @@ -72,8 +77,9 @@ func NewServer(addr string, group []*Config) (*Server, error) { log.D.Set() } 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 for key, secret := range site.TsigSecret { @@ -88,6 +94,12 @@ func NewServer(addr string, group []*Config) (*Server, error) { // register the *handler* also 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" { // we have to stash away the plugin, not the // 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 { - if h, ok := s.zones[q[off:]]; ok { - if h.pluginChain == nil { // zone defined, but has not got any plugins - 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) { - errorFunc(s.Addr, w, r, rcode) + if z, ok := s.zones[q[off:]]; ok { + for _, h := range z { + if h.pluginChain == nil { // zone defined, but has not got any plugins + errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) + return + } + + 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) + } + 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) 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. - if h, ok := s.zones["."]; ok && h.pluginChain != nil { - rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) - if !plugin.ClientWrite(rcode) { - errorFunc(s.Addr, w, r, rcode) + if z, ok := s.zones["."]; ok { + for _, h := range z { + if h.pluginChain == nil { + 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. 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 // and any relevant information, assuming Quiet is false. func (s *Server) OnStartupComplete() { @@ -341,7 +396,7 @@ func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int answer.SetRcode(r, rc) 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) } @@ -357,6 +412,9 @@ type ( // LoopKey is the context key to detect server wide loops. 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. diff --git a/core/dnsserver/server_grpc.go b/core/dnsserver/server_grpc.go index 2ba4d6a4d..9d7a95ace 100644 --- a/core/dnsserver/server_grpc.go +++ b/core/dnsserver/server_grpc.go @@ -37,9 +37,11 @@ func NewServergRPC(addr string, group []*Config) (*ServergRPC, error) { // The *tls* plugin must make sure that multiple conflicting // TLS configuration returns an error: it can only be specified once. var tlsConfig *tls.Config - for _, conf := range s.zones { - // Should we error if some configs *don't* have TLS? - tlsConfig = conf.TLSConfig + for _, z := range s.zones { + for _, conf := range z { + // 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 // or the upgrade won't happen. diff --git a/core/dnsserver/server_https.go b/core/dnsserver/server_https.go index 8904da57e..eda39c140 100644 --- a/core/dnsserver/server_https.go +++ b/core/dnsserver/server_https.go @@ -50,9 +50,11 @@ func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) { // The *tls* plugin must make sure that multiple conflicting // TLS configuration returns an error: it can only be specified once. var tlsConfig *tls.Config - for _, conf := range s.zones { - // Should we error if some configs *don't* have TLS? - tlsConfig = conf.TLSConfig + for _, z := range s.zones { + for _, conf := range z { + // 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 @@ -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. var validator func(*http.Request) bool - for _, conf := range s.zones { - validator = conf.HTTPRequestValidateFunc + for _, z := range s.zones { + for _, conf := range z { + validator = conf.HTTPRequestValidateFunc + } } if validator == nil { validator = func(r *http.Request) bool { return r.URL.Path == doh.Path } diff --git a/core/dnsserver/server_tls.go b/core/dnsserver/server_tls.go index 1c53c4e3c..6fff61d5c 100644 --- a/core/dnsserver/server_tls.go +++ b/core/dnsserver/server_tls.go @@ -28,9 +28,11 @@ func NewServerTLS(addr string, group []*Config) (*ServerTLS, error) { // The *tls* plugin must make sure that multiple conflicting // TLS configuration returns an error: it can only be specified once. var tlsConfig *tls.Config - for _, conf := range s.zones { - // Should we error if some configs *don't* have TLS? - tlsConfig = conf.TLSConfig + for _, z := range s.zones { + for _, conf := range z { + // Should we error if some configs *don't* have TLS? + tlsConfig = conf.TLSConfig + } } return &ServerTLS{Server: s, tlsConfig: tlsConfig}, nil diff --git a/core/dnsserver/view.go b/core/dnsserver/view.go new file mode 100644 index 000000000..ac797839d --- /dev/null +++ b/core/dnsserver/view.go @@ -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 +} diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index 53168be86..38425fb06 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -60,4 +60,5 @@ var Directives = []string{ "whoami", "on", "sign", + "view", } diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index 45bfb5415..08003f460 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -53,5 +53,6 @@ import ( _ "github.com/coredns/coredns/plugin/trace" _ "github.com/coredns/coredns/plugin/transfer" _ "github.com/coredns/coredns/plugin/tsig" + _ "github.com/coredns/coredns/plugin/view" _ "github.com/coredns/coredns/plugin/whoami" ) diff --git a/go.mod b/go.mod index 9d9fe775e..458c4ee41 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( 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/azure/auth v0.5.11 + github.com/antonmedv/expr v1.9.0 github.com/apparentlymart/go-cidr v1.1.0 github.com/aws/aws-sdk-go v1.44.91 github.com/coredns/caddy v1.1.1 diff --git a/go.sum b/go.sum index 81d6246fb..c2b4a2e56 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw= 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.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 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/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= @@ -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.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-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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 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/ghodss/yaml v0.0.0-20150909031657-73d445a93680/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.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/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.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 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/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/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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 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 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/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/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= @@ -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/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-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.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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-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-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-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/plugin.cfg b/plugin.cfg index 46a7df4c1..5632a3b97 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -69,3 +69,4 @@ erratic:erratic whoami:whoami on:github.com/coredns/caddy/onevent sign:sign +view:view diff --git a/plugin/cache/README.md b/plugin/cache/README.md index 602072656..562f5bd9a 100644 --- a/plugin/cache/README.md +++ b/plugin/cache/README.md @@ -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: -* `coredns_cache_entries{server, type, zones}` - Total elements in the cache by cache type. -* `coredns_cache_hits_total{server, type, zones}` - 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_requests_total{server, zones}` - Counter of cache requests. -* `coredns_cache_prefetch_total{server, zones}` - 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_served_stale_total{server, zones}` - Counter of requests served from stale cache entries. -* `coredns_cache_evictions_total{server, type, zones}` - Counter of cache evictions. +* `coredns_cache_entries{server, type, zones, view}` - Total elements in the cache by cache type. +* `coredns_cache_hits_total{server, type, zones, view}` - Counter of cache hits by cache type. +* `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, view}` - Counter of cache requests. +* `coredns_cache_prefetch_total{server, zones, view}` - Counter of times the cache has prefetched a cached item. +* `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, view}` - Counter of requests served from stale cache entries. +* `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 prometheus plugin for documentation. diff --git a/plugin/cache/cache.go b/plugin/cache/cache.go index bfd8c1576..b4767937d 100644 --- a/plugin/cache/cache.go +++ b/plugin/cache/cache.go @@ -22,6 +22,7 @@ type Cache struct { Zones []string zonesMetricLabel string + viewMetricLabel string ncache *cache.Cache ncap int @@ -177,11 +178,11 @@ func (w *ResponseWriter) WriteMsg(res *dns.Msg) error { if hasKey && duration > 0 { if w.state.Match(res) { w.set(res, key, mt, duration) - cacheSize.WithLabelValues(w.server, Success, w.zonesMetricLabel).Set(float64(w.pcache.Len())) - cacheSize.WithLabelValues(w.server, Denial, w.zonesMetricLabel).Set(float64(w.ncache.Len())) + cacheSize.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.pcache.Len())) + cacheSize.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.ncache.Len())) } else { // 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() } 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 if w.prefetch { @@ -236,7 +237,7 @@ func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration i.wildcard = w.wildcardFunc() } 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: diff --git a/plugin/cache/handler.go b/plugin/cache/handler.go index 3d2f43904..ec2135e8c 100644 --- a/plugin/cache/handler.go +++ b/plugin/cache/handler.go @@ -60,7 +60,7 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) cw := newPrefetchResponseWriter(server, state, c) 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) { cw := newPrefetchResponseWriter(server, state, c) 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) { - cachePrefetches.WithLabelValues(cw.server, c.zonesMetricLabel).Inc() + cachePrefetches.WithLabelValues(cw.server, c.zonesMetricLabel, c.viewMetricLabel).Inc() c.doRefresh(ctx, state, cw) // 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. func (c *Cache) getIgnoreTTL(now time.Time, state request.Request, server string) *item { 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 { itm := i.(*item) ttl := itm.ttl(now) 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) } } @@ -136,11 +136,11 @@ func (c *Cache) getIgnoreTTL(now time.Time, state request.Request, server string itm := i.(*item) ttl := itm.ttl(now) 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) } } - cacheMisses.WithLabelValues(server, c.zonesMetricLabel).Inc() + cacheMisses.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc() return nil } diff --git a/plugin/cache/metrics.go b/plugin/cache/metrics.go index 2eb573113..77edb0286 100644 --- a/plugin/cache/metrics.go +++ b/plugin/cache/metrics.go @@ -14,54 +14,54 @@ var ( Subsystem: "cache", Name: "entries", 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 = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "requests_total", Help: "The count of cache requests.", - }, []string{"server", "zones"}) + }, []string{"server", "zones", "view"}) // cacheHits is counter of cache hits by cache type. cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "hits_total", Help: "The count of cache hits.", - }, []string{"server", "type", "zones"}) + }, []string{"server", "type", "zones", "view"}) // cacheMisses is the counter of cache misses. - Deprecated cacheMisses = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "misses_total", 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 = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "prefetch_total", 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 = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "drops_total", 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 = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "served_stale_total", 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 = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "evictions_total", Help: "The count of cache evictions.", - }, []string{"server", "type", "zones"}) + }, []string{"server", "type", "zones", "view"}) ) diff --git a/plugin/cache/setup.go b/plugin/cache/setup.go index a1ce255a9..6a537d986 100644 --- a/plugin/cache/setup.go +++ b/plugin/cache/setup.go @@ -23,6 +23,12 @@ func setup(c *caddy.Controller) error { if err != nil { 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 { ca.Next = next return ca diff --git a/plugin/metadata/metadata.go b/plugin/metadata/metadata.go index c2fe93cfb..58e5ce2e2 100644 --- a/plugin/metadata/metadata.go +++ b/plugin/metadata/metadata.go @@ -27,17 +27,18 @@ func ContextWithMetadata(ctx context.Context) context.Context { // ServeDNS implements the plugin.Handler interface. 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()) != "" { // Go through all Providers and collect metadata. for _, p := range m.Providers { ctx = p.Metadata(ctx, state) } } - - rcode, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, w, r) - - return rcode, err + return ctx } diff --git a/plugin/metadata/metadata_test.go b/plugin/metadata/metadata_test.go index 3dc507de0..6b8da6d00 100644 --- a/plugin/metadata/metadata_test.go +++ b/plugin/metadata/metadata_test.go @@ -47,7 +47,10 @@ func TestMetadataServeDNS(t *testing.T) { } 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 for _, expected := range expectedMetadata { diff --git a/plugin/metrics/README.md b/plugin/metrics/README.md index b107ca405..e5c3b2bda 100644 --- a/plugin/metrics/README.md +++ b/plugin/metrics/README.md @@ -14,12 +14,12 @@ the following metrics are exported: * `coredns_build_info{version, revision, goversion}` - info about CoreDNS itself. * `coredns_panics_total{}` - total number of panics. -* `coredns_dns_requests_total{server, zone, proto, family, type}` - total query count. -* `coredns_dns_request_duration_seconds{server, zone, type}` - duration to process each query. -* `coredns_dns_request_size_bytes{server, zone, proto}` - size of the request in bytes. -* `coredns_dns_do_requests_total{server, zone}` - queries that have the DO bit set -* `coredns_dns_response_size_bytes{server, zone, proto}` - response size in bytes. -* `coredns_dns_responses_total{server, zone, rcode, plugin}` - response per zone, rcode and plugin. +* `coredns_dns_requests_total{server, zone, view, proto, family, type}` - total query count. +* `coredns_dns_request_duration_seconds{server, zone, view, type}` - duration to process each query. +* `coredns_dns_request_size_bytes{server, zone, view, proto}` - size of the request in bytes. +* `coredns_dns_do_requests_total{server, view, zone}` - queries that have the DO bit set +* `coredns_dns_response_size_bytes{server, zone, view, proto}` - response size in bytes. +* `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_plugin_enabled{server, zone, name}` - indicates whether a plugin is enabled on per server and zone basis. diff --git a/plugin/metrics/context.go b/plugin/metrics/context.go index da6bdb12d..ae2856dd5 100644 --- a/plugin/metrics/context.go +++ b/plugin/metrics/context.go @@ -22,3 +22,16 @@ func WithServer(ctx context.Context) string { } 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: +// +// .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) +} diff --git a/plugin/metrics/handler.go b/plugin/metrics/handler.go index 90db76181..41da69011 100644 --- a/plugin/metrics/handler.go +++ b/plugin/metrics/handler.go @@ -34,7 +34,7 @@ func (m *Metrics) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg rc = status } 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 } diff --git a/plugin/metrics/vars/report.go b/plugin/metrics/vars/report.go index 9761f626f..92f6bc163 100644 --- a/plugin/metrics/vars/report.go +++ b/plugin/metrics/vars/report.go @@ -9,7 +9,7 @@ import ( // 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 // 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. net := req.Proto() fam := "1" @@ -18,16 +18,16 @@ func Report(server string, req request.Request, zone, rcode, plugin string, size } if req.Do() { - RequestDo.WithLabelValues(server, zone).Inc() + RequestDo.WithLabelValues(server, zone, view).Inc() } 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)) - RequestSize.WithLabelValues(server, zone, net).Observe(float64(req.Len())) + ResponseSize.WithLabelValues(server, zone, view, net).Observe(float64(size)) + 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() } diff --git a/plugin/metrics/vars/vars.go b/plugin/metrics/vars/vars.go index c7bf74c6b..f4ed76bae 100644 --- a/plugin/metrics/vars/vars.go +++ b/plugin/metrics/vars/vars.go @@ -14,7 +14,7 @@ var ( Subsystem: subsystem, Name: "requests_total", 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{ Namespace: plugin.Namespace, @@ -22,7 +22,7 @@ var ( Name: "request_duration_seconds", Buckets: plugin.TimeBuckets, Help: "Histogram of the time (in seconds) each request took per zone.", - }, []string{"server", "zone"}) + }, []string{"server", "zone", "view"}) RequestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: plugin.Namespace, @@ -30,14 +30,14 @@ var ( Name: "request_size_bytes", 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}, - }, []string{"server", "zone", "proto"}) + }, []string{"server", "zone", "view", "proto"}) RequestDo = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: subsystem, Name: "do_requests_total", Help: "Counter of DNS requests with DO bit set per zone.", - }, []string{"server", "zone"}) + }, []string{"server", "zone", "view"}) ResponseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: plugin.Namespace, @@ -45,14 +45,14 @@ var ( Name: "response_size_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}, - }, []string{"server", "zone", "proto"}) + }, []string{"server", "zone", "view", "proto"}) ResponseRcode = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: subsystem, Name: "responses_total", Help: "Counter of response status codes.", - }, []string{"server", "zone", "rcode", "plugin"}) + }, []string{"server", "zone", "view", "rcode", "plugin"}) Panic = promauto.NewCounter(prometheus.CounterOpts{ Namespace: plugin.Namespace, diff --git a/plugin/pkg/expression/expression.go b/plugin/pkg/expression/expression.go new file mode 100644 index 000000000..dad38fefd --- /dev/null +++ b/plugin/pkg/expression/expression.go @@ -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, + } +} diff --git a/plugin/pkg/expression/expression_test.go b/plugin/pkg/expression/expression_test.go new file mode 100644 index 000000000..b39c67940 --- /dev/null +++ b/plugin/pkg/expression/expression_test.go @@ -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 + } + } +} diff --git a/plugin/pkg/replacer/replacer_test.go b/plugin/pkg/replacer/replacer_test.go index e428aad9a..28bb08d7a 100644 --- a/plugin/pkg/replacer/replacer_test.go +++ b/plugin/pkg/replacer/replacer_test.go @@ -340,13 +340,12 @@ func TestMetadataReplacement(t *testing.T) { 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{}) r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeHINFO) + ctx := m.Collect(context.TODO(), request.Request{W: w, Req: r}) + repl := New() state := request.Request{W: w, Req: r} diff --git a/plugin/rewrite/rewrite_test.go b/plugin/rewrite/rewrite_test.go index ae5576ab5..03d4fff1d 100644 --- a/plugin/rewrite/rewrite_test.go +++ b/plugin/rewrite/rewrite_test.go @@ -604,8 +604,8 @@ func TestRewriteEDNS0LocalVariable(t *testing.T) { } rw.Rules = []Rule{r} - ctx := context.TODO() rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := meta.Collect(context.TODO(), request.Request{W: rec, Req: m}) meta.ServeDNS(ctx, rec, m) resp := rec.Msg diff --git a/plugin/view/README.md b/plugin/view/README.md new file mode 100644 index 000000000..cff6a9abe --- /dev/null +++ b/plugin/view/README.md @@ -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 diff --git a/plugin/view/metadata.go b/plugin/view/metadata.go new file mode 100644 index 000000000..6ee9bc069 --- /dev/null +++ b/plugin/view/metadata.go @@ -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 +} diff --git a/plugin/view/setup.go b/plugin/view/setup.go new file mode 100644 index 000000000..34ecc7960 --- /dev/null +++ b/plugin/view/setup.go @@ -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 +} diff --git a/plugin/view/setup_test.go b/plugin/view/setup_test.go new file mode 100644 index 000000000..7c7838070 --- /dev/null +++ b/plugin/view/setup_test.go @@ -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) + } + } +} diff --git a/plugin/view/view.go b/plugin/view/view.go new file mode 100644 index 000000000..448a63afa --- /dev/null +++ b/plugin/view/view.go @@ -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) +} diff --git a/test/view_test.go b/test/view_test.go new file mode 100644 index 000000000..f68595380 --- /dev/null +++ b/test/view_test.go @@ -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) + } + }) +}