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

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

Signed-off-by: Chris O'Haver <cohaver@infoblox.com>
This commit is contained in:
Chris O'Haver
2022-09-08 14:56:27 -04:00
committed by GitHub
parent 1f0a41a665
commit b56b080a7c
36 changed files with 880 additions and 114 deletions

View File

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

View File

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

View File

@@ -340,13 +340,12 @@ func TestMetadataReplacement(t *testing.T) {
Next: next,
}
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}