Commit 77454624 authored by Francois Tur's avatar Francois Tur Committed by Miek Gieben

plugin/rewrite - extend edns0 local variable support with metadata (#1928)

* - add support of metadata values for edns0 local variables

* - comments from review.

* - simplify label check. Add UT

* - enhance check for Labels, add UT
- remove IsMetadataSet

* - edns0 variable - if variable is not found just ignore the rewrite.
parent 6ec19783
......@@ -30,8 +30,8 @@ func (m *testHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns
func TestMetadataServeDNS(t *testing.T) {
expectedMetadata := []testProvider{
testProvider{"test/key1": func() string { return "testvalue1" }},
testProvider{"test/key2": func() string { return "two" }, "test/key3": func() string { return "testvalue3" }},
{"test/key1": func() string { return "testvalue1" }},
{"test/key2": func() string { return "two" }, "test/key3": func() string { return "testvalue3" }},
}
// Create fake Providers based on expectedMetadata
providers := []Provider{}
......@@ -52,6 +52,9 @@ func TestMetadataServeDNS(t *testing.T) {
for _, expected := range expectedMetadata {
for label, expVal := range expected {
if !IsLabel(label) {
t.Errorf("Expected label %s is not considered a valid label", label)
}
val := ValueFunc(nctx, label)
if val() != expVal() {
t.Errorf("Expected value %s for %s, but got %s", expVal(), label, val())
......@@ -59,3 +62,26 @@ func TestMetadataServeDNS(t *testing.T) {
}
}
}
func TestLabelFormat(t *testing.T) {
labels := []struct {
label string
isValid bool
}{
{"plugin/LABEL", true},
{"p/LABEL", true},
{"plugin/L", true},
{"LABEL", false},
{"plugin.LABEL", false},
{"/NO-PLUGIN-NOT-ACCEPTED", false},
{"ONLY-PLUGIN-NOT-ACCEPTED/", false},
{"PLUGIN/LABEL/SUB-LABEL", false},
{"/", false},
}
for _, test := range labels {
if IsLabel(test.label) != test.isValid {
t.Errorf("Label %v is expected to have this validaty : %v - and has the opposite", test.label, test.isValid)
}
}
}
......@@ -32,6 +32,7 @@ package metadata
import (
"context"
"strings"
"github.com/coredns/coredns/request"
)
......@@ -48,6 +49,21 @@ type Provider interface {
// Func is the type of function in the metadata, when called they return the value of the label.
type Func func() string
// IsLabel check that the provided name looks like a valid label name
func IsLabel(label string) bool {
p := strings.Index(label, "/")
if p <= 0 || p >= len(label)-1 {
// cannot accept namespace empty nor label empty
return false
}
if strings.LastIndex(label, "/") != p {
// several slash in the Label
return false
}
return true
}
// Labels returns all metadata keys stored in the context. These label names should be named
// as: plugin/NAME, where NAME is something descriptive.
func Labels(ctx context.Context) []string {
......
......@@ -209,12 +209,24 @@ rewrites the first local option with code 0xffee, setting the data to "abcd". Eq
* A variable data is specified with a pair of curly brackets `{}`. Following are the supported variables:
{qname}, {qtype}, {client_ip}, {client_port}, {protocol}, {server_ip}, {server_port}.
Example:
* If the metadata plugin is enabled, then labels are supported as variables if they are presented within curly brackets.
the variable data will be filled with the value associated with that label. If that label is not provided,
the variable will be silently substitute by an empty string.
Examples:
~~~
rewrite edns0 local set 0xffee {client_ip}
~~~
The following example uses metadata and an imaginary "some-plugin" that would provide "some-label" as metadata information.
~~~
metadata
some-plugin
rewrite edns0 local set 0xffee {some-plugin/some-label}
~~~
### EDNS0_NSID
This has no fields; it will add an NSID option with an empty string for the NSID. If the option already exists
......
package rewrite
import (
"context"
"fmt"
"strings"
......@@ -29,7 +30,7 @@ func newClassRule(nextAction string, args ...string) (Rule, error) {
}
// Rewrite rewrites the the current request.
func (rule *classRule) Rewrite(state request.Request) Result {
func (rule *classRule) Rewrite(ctx context.Context, state request.Request) Result {
if rule.fromClass > 0 && rule.toClass > 0 {
if state.Req.Question[0].Qclass == rule.fromClass {
state.Req.Question[0].Qclass = rule.toClass
......
......@@ -2,12 +2,14 @@
package rewrite
import (
"context"
"encoding/hex"
"fmt"
"net"
"strconv"
"strings"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
......@@ -46,7 +48,7 @@ func setupEdns0Opt(r *dns.Msg) *dns.OPT {
}
// Rewrite will alter the request EDNS0 NSID option
func (rule *edns0NsidRule) Rewrite(state request.Request) Result {
func (rule *edns0NsidRule) Rewrite(ctx context.Context, state request.Request) Result {
o := setupEdns0Opt(state.Req)
for _, s := range o.Option {
......@@ -74,7 +76,7 @@ func (rule *edns0NsidRule) Mode() string { return rule.mode }
func (rule *edns0NsidRule) GetResponseRule() ResponseRule { return ResponseRule{} }
// Rewrite will alter the request EDNS0 local options.
func (rule *edns0LocalRule) Rewrite(state request.Request) Result {
func (rule *edns0LocalRule) Rewrite(ctx context.Context, state request.Request) Result {
o := setupEdns0Opt(state.Req)
for _, s := range o.Option {
......@@ -174,7 +176,7 @@ func newEdns0VariableRule(mode, action, code, variable string) (*edns0VariableRu
}
// ruleData returns the data specified by the variable.
func (rule *edns0VariableRule) ruleData(state request.Request) ([]byte, error) {
func (rule *edns0VariableRule) ruleData(ctx context.Context, state request.Request) ([]byte, error) {
switch rule.variable {
case queryName:
......@@ -199,12 +201,17 @@ func (rule *edns0VariableRule) ruleData(state request.Request) ([]byte, error) {
return []byte(state.Proto()), nil
}
fetcher := metadata.ValueFunc(ctx, rule.variable[1:len(rule.variable)-1])
if fetcher != nil {
return []byte(fetcher()), nil
}
return nil, fmt.Errorf("unable to extract data for variable %s", rule.variable)
}
// Rewrite will alter the request EDNS0 local options with specified variables.
func (rule *edns0VariableRule) Rewrite(state request.Request) Result {
data, err := rule.ruleData(state)
func (rule *edns0VariableRule) Rewrite(ctx context.Context, state request.Request) Result {
data, err := rule.ruleData(ctx, state)
if err != nil || data == nil {
return RewriteIgnored
}
......@@ -249,6 +256,10 @@ func isValidVariable(variable string) bool {
serverPort:
return true
}
// we cannot validate the labels of metadata - but we can verify it has the syntax of a label
if strings.HasPrefix(variable, "{") && strings.HasSuffix(variable, "}") && metadata.IsLabel(variable[1:len(variable)-1]) {
return true
}
return false
}
......@@ -310,7 +321,7 @@ func (rule *edns0SubnetRule) fillEcsData(state request.Request, ecs *dns.EDNS0_S
}
// Rewrite will alter the request EDNS0 subnet option.
func (rule *edns0SubnetRule) Rewrite(state request.Request) Result {
func (rule *edns0SubnetRule) Rewrite(ctx context.Context, state request.Request) Result {
o := setupEdns0Opt(state.Req)
for _, s := range o.Option {
......
package rewrite
import (
"context"
"fmt"
"regexp"
"strconv"
......@@ -56,7 +57,7 @@ const (
// Rewrite rewrites the current request based upon exact match of the name
// in the question section of the request.
func (rule *nameRule) Rewrite(state request.Request) Result {
func (rule *nameRule) Rewrite(ctx context.Context, state request.Request) Result {
if rule.From == state.Name() {
state.Req.Question[0].Name = rule.To
return RewriteDone
......@@ -65,7 +66,7 @@ func (rule *nameRule) Rewrite(state request.Request) Result {
}
// Rewrite rewrites the current request when the name begins with the matching string.
func (rule *prefixNameRule) Rewrite(state request.Request) Result {
func (rule *prefixNameRule) Rewrite(ctx context.Context, state request.Request) Result {
if strings.HasPrefix(state.Name(), rule.Prefix) {
state.Req.Question[0].Name = rule.Replacement + strings.TrimLeft(state.Name(), rule.Prefix)
return RewriteDone
......@@ -74,7 +75,7 @@ func (rule *prefixNameRule) Rewrite(state request.Request) Result {
}
// Rewrite rewrites the current request when the name ends with the matching string.
func (rule *suffixNameRule) Rewrite(state request.Request) Result {
func (rule *suffixNameRule) Rewrite(ctx context.Context, state request.Request) Result {
if strings.HasSuffix(state.Name(), rule.Suffix) {
state.Req.Question[0].Name = strings.TrimRight(state.Name(), rule.Suffix) + rule.Replacement
return RewriteDone
......@@ -84,7 +85,7 @@ func (rule *suffixNameRule) Rewrite(state request.Request) Result {
// Rewrite rewrites the current request based upon partial match of the
// name in the question section of the request.
func (rule *substringNameRule) Rewrite(state request.Request) Result {
func (rule *substringNameRule) Rewrite(ctx context.Context, state request.Request) Result {
if strings.Contains(state.Name(), rule.Substring) {
state.Req.Question[0].Name = strings.Replace(state.Name(), rule.Substring, rule.Replacement, -1)
return RewriteDone
......@@ -94,7 +95,7 @@ func (rule *substringNameRule) Rewrite(state request.Request) Result {
// Rewrite rewrites the current request when the name in the question
// section of the request matches a regular expression.
func (rule *regexNameRule) Rewrite(state request.Request) Result {
func (rule *regexNameRule) Rewrite(ctx context.Context, state request.Request) Result {
regexGroups := rule.Pattern.FindStringSubmatch(state.Name())
if len(regexGroups) == 0 {
return RewriteIgnored
......
......@@ -42,7 +42,7 @@ func (rw Rewrite) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
state := request.Request{W: w, Req: r}
for _, rule := range rw.Rules {
switch result := rule.Rewrite(state); result {
switch result := rule.Rewrite(ctx, state); result {
case RewriteDone:
respRule := rule.GetResponseRule()
if respRule.Active == true {
......@@ -71,7 +71,7 @@ func (rw Rewrite) Name() string { return "rewrite" }
// Rule describes a rewrite rule.
type Rule interface {
// Rewrite rewrites the current request.
Rewrite(state request.Request) Result
Rewrite(ctx context.Context, state request.Request) Result
// Mode returns the processing mode stop or continue.
Mode() string
// GetResponseRule returns the rule to rewrite response with, if any.
......
......@@ -7,8 +7,10 @@ import (
"testing"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
......@@ -434,12 +436,32 @@ func optsEqual(a, b []dns.EDNS0) bool {
return true
}
type testProvider map[string]metadata.Func
func (tp testProvider) Metadata(ctx context.Context, state request.Request) context.Context {
for k, v := range tp {
metadata.SetValueFunc(ctx, k, v)
}
return ctx
}
func TestRewriteEDNS0LocalVariable(t *testing.T) {
rw := Rewrite{
Next: plugin.HandlerFunc(msgPrinter),
noRevert: true,
}
expectedMetadata := []metadata.Provider{
testProvider{"test/label": func() string { return "my-value" }},
testProvider{"test/empty": func() string { return "" }},
}
meta := metadata.Metadata{
Zones: []string{"."},
Providers: expectedMetadata,
Next: &rw,
}
// test.ResponseWriter has the following values:
// The remote will always be 10.240.0.1 and port 40212.
// The local address is always 127.0.0.1 and port 53.
......@@ -492,9 +514,26 @@ func TestRewriteEDNS0LocalVariable(t *testing.T) {
[]dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x7F, 0x00, 0x00, 0x01}}},
true,
},
{
[]dns.EDNS0{},
[]string{"local", "set", "0xffee", "{test/label}"},
[]dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("my-value")}},
true,
},
{
[]dns.EDNS0{},
[]string{"local", "set", "0xffee", "{test/empty}"},
[]dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("")}},
true,
},
{
[]dns.EDNS0{},
[]string{"local", "set", "0xffee", "{test/does-not-exist}"},
nil,
false,
},
}
ctx := context.TODO()
for i, tc := range tests {
m := new(dns.Msg)
m.SetQuestion("example.com.", dns.TypeA)
......@@ -506,16 +545,19 @@ func TestRewriteEDNS0LocalVariable(t *testing.T) {
}
rw.Rules = []Rule{r}
ctx := context.TODO()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
rw.ServeDNS(ctx, rec, m)
meta.ServeDNS(ctx, rec, m)
resp := rec.Msg
o := resp.IsEdns0()
o.SetDo(tc.doBool)
if o == nil {
t.Errorf("Test %d: EDNS0 options not set", i)
if tc.toOpts != nil {
t.Errorf("Test %d: EDNS0 options not set", i)
}
continue
}
o.SetDo(tc.doBool)
if o.Do() != tc.doBool {
t.Errorf("Test %d: Expected %v but got %v", i, tc.doBool, o.Do())
}
......
......@@ -2,6 +2,7 @@
package rewrite
import (
"context"
"fmt"
"strings"
......@@ -30,7 +31,7 @@ func newTypeRule(nextAction string, args ...string) (Rule, error) {
}
// Rewrite rewrites the the current request.
func (rule *typeRule) Rewrite(state request.Request) Result {
func (rule *typeRule) Rewrite(ctx context.Context, state request.Request) Result {
if rule.fromType > 0 && rule.toType > 0 {
if state.QType() == rule.fromType {
state.Req.Question[0].Qtype = rule.toType
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment