mirror of
https://github.com/coredns/coredns.git
synced 2025-11-03 10:43:20 -05:00
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:
16
plugin/cache/README.md
vendored
16
plugin/cache/README.md
vendored
@@ -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.
|
||||
|
||||
11
plugin/cache/cache.go
vendored
11
plugin/cache/cache.go
vendored
@@ -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:
|
||||
|
||||
12
plugin/cache/handler.go
vendored
12
plugin/cache/handler.go
vendored
@@ -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
|
||||
}
|
||||
|
||||
|
||||
16
plugin/cache/metrics.go
vendored
16
plugin/cache/metrics.go
vendored
@@ -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"})
|
||||
)
|
||||
|
||||
6
plugin/cache/setup.go
vendored
6
plugin/cache/setup.go
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
//
|
||||
// <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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
47
plugin/pkg/expression/expression.go
Normal file
47
plugin/pkg/expression/expression.go
Normal 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,
|
||||
}
|
||||
}
|
||||
73
plugin/pkg/expression/expression_test.go
Normal file
73
plugin/pkg/expression/expression_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
135
plugin/view/README.md
Normal file
135
plugin/view/README.md
Normal 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
16
plugin/view/metadata.go
Normal 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
65
plugin/view/setup.go
Normal 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
38
plugin/view/setup_test.go
Normal 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
48
plugin/view/view.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user