mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 08:14:18 -04: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:
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