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.
This commit is contained in:
Francois Tur
2018-07-08 03:18:01 -04:00
committed by Miek Gieben
parent 6ec1978340
commit 7745462430
9 changed files with 132 additions and 22 deletions

View File

@@ -30,8 +30,8 @@ func (m *testHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns
func TestMetadataServeDNS(t *testing.T) { func TestMetadataServeDNS(t *testing.T) {
expectedMetadata := []testProvider{ expectedMetadata := []testProvider{
testProvider{"test/key1": func() string { return "testvalue1" }}, {"test/key1": func() string { return "testvalue1" }},
testProvider{"test/key2": func() string { return "two" }, "test/key3": func() string { return "testvalue3" }}, {"test/key2": func() string { return "two" }, "test/key3": func() string { return "testvalue3" }},
} }
// Create fake Providers based on expectedMetadata // Create fake Providers based on expectedMetadata
providers := []Provider{} providers := []Provider{}
@@ -52,6 +52,9 @@ func TestMetadataServeDNS(t *testing.T) {
for _, expected := range expectedMetadata { for _, expected := range expectedMetadata {
for label, expVal := range expected { for label, expVal := range expected {
if !IsLabel(label) {
t.Errorf("Expected label %s is not considered a valid label", label)
}
val := ValueFunc(nctx, label) val := ValueFunc(nctx, label)
if val() != expVal() { if val() != expVal() {
t.Errorf("Expected value %s for %s, but got %s", expVal(), label, val()) 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)
}
}
}

View File

@@ -32,6 +32,7 @@ package metadata
import ( import (
"context" "context"
"strings"
"github.com/coredns/coredns/request" "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. // Func is the type of function in the metadata, when called they return the value of the label.
type Func func() string 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 // Labels returns all metadata keys stored in the context. These label names should be named
// as: plugin/NAME, where NAME is something descriptive. // as: plugin/NAME, where NAME is something descriptive.
func Labels(ctx context.Context) []string { func Labels(ctx context.Context) []string {

View File

@@ -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: * 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}. {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} 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 ### EDNS0_NSID
This has no fields; it will add an NSID option with an empty string for the NSID. If the option already exists This has no fields; it will add an NSID option with an empty string for the NSID. If the option already exists

View File

@@ -1,6 +1,7 @@
package rewrite package rewrite
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
@@ -29,7 +30,7 @@ func newClassRule(nextAction string, args ...string) (Rule, error) {
} }
// Rewrite rewrites the the current request. // 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 rule.fromClass > 0 && rule.toClass > 0 {
if state.Req.Question[0].Qclass == rule.fromClass { if state.Req.Question[0].Qclass == rule.fromClass {
state.Req.Question[0].Qclass = rule.toClass state.Req.Question[0].Qclass = rule.toClass

View File

@@ -2,12 +2,14 @@
package rewrite package rewrite
import ( import (
"context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
"strings" "strings"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/request" "github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
@@ -46,7 +48,7 @@ func setupEdns0Opt(r *dns.Msg) *dns.OPT {
} }
// Rewrite will alter the request EDNS0 NSID option // 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) o := setupEdns0Opt(state.Req)
for _, s := range o.Option { for _, s := range o.Option {
@@ -74,7 +76,7 @@ func (rule *edns0NsidRule) Mode() string { return rule.mode }
func (rule *edns0NsidRule) GetResponseRule() ResponseRule { return ResponseRule{} } func (rule *edns0NsidRule) GetResponseRule() ResponseRule { return ResponseRule{} }
// Rewrite will alter the request EDNS0 local options. // 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) o := setupEdns0Opt(state.Req)
for _, s := range o.Option { 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. // 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 { switch rule.variable {
case queryName: case queryName:
@@ -199,12 +201,17 @@ func (rule *edns0VariableRule) ruleData(state request.Request) ([]byte, error) {
return []byte(state.Proto()), nil 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) return nil, fmt.Errorf("unable to extract data for variable %s", rule.variable)
} }
// Rewrite will alter the request EDNS0 local options with specified variables. // Rewrite will alter the request EDNS0 local options with specified variables.
func (rule *edns0VariableRule) Rewrite(state request.Request) Result { func (rule *edns0VariableRule) Rewrite(ctx context.Context, state request.Request) Result {
data, err := rule.ruleData(state) data, err := rule.ruleData(ctx, state)
if err != nil || data == nil { if err != nil || data == nil {
return RewriteIgnored return RewriteIgnored
} }
@@ -249,6 +256,10 @@ func isValidVariable(variable string) bool {
serverPort: serverPort:
return true 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 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. // 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) o := setupEdns0Opt(state.Req)
for _, s := range o.Option { for _, s := range o.Option {

View File

@@ -1,6 +1,7 @@
package rewrite package rewrite
import ( import (
"context"
"fmt" "fmt"
"regexp" "regexp"
"strconv" "strconv"
@@ -56,7 +57,7 @@ const (
// Rewrite rewrites the current request based upon exact match of the name // Rewrite rewrites the current request based upon exact match of the name
// in the question section of the request. // 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() { if rule.From == state.Name() {
state.Req.Question[0].Name = rule.To state.Req.Question[0].Name = rule.To
return RewriteDone 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. // 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) { if strings.HasPrefix(state.Name(), rule.Prefix) {
state.Req.Question[0].Name = rule.Replacement + strings.TrimLeft(state.Name(), rule.Prefix) state.Req.Question[0].Name = rule.Replacement + strings.TrimLeft(state.Name(), rule.Prefix)
return RewriteDone 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. // 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) { if strings.HasSuffix(state.Name(), rule.Suffix) {
state.Req.Question[0].Name = strings.TrimRight(state.Name(), rule.Suffix) + rule.Replacement state.Req.Question[0].Name = strings.TrimRight(state.Name(), rule.Suffix) + rule.Replacement
return RewriteDone 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 // Rewrite rewrites the current request based upon partial match of the
// name in the question section of the request. // 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) { if strings.Contains(state.Name(), rule.Substring) {
state.Req.Question[0].Name = strings.Replace(state.Name(), rule.Substring, rule.Replacement, -1) state.Req.Question[0].Name = strings.Replace(state.Name(), rule.Substring, rule.Replacement, -1)
return RewriteDone 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 // Rewrite rewrites the current request when the name in the question
// section of the request matches a regular expression. // 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()) regexGroups := rule.Pattern.FindStringSubmatch(state.Name())
if len(regexGroups) == 0 { if len(regexGroups) == 0 {
return RewriteIgnored return RewriteIgnored

View File

@@ -42,7 +42,7 @@ func (rw Rewrite) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
state := request.Request{W: w, Req: r} state := request.Request{W: w, Req: r}
for _, rule := range rw.Rules { for _, rule := range rw.Rules {
switch result := rule.Rewrite(state); result { switch result := rule.Rewrite(ctx, state); result {
case RewriteDone: case RewriteDone:
respRule := rule.GetResponseRule() respRule := rule.GetResponseRule()
if respRule.Active == true { if respRule.Active == true {
@@ -71,7 +71,7 @@ func (rw Rewrite) Name() string { return "rewrite" }
// Rule describes a rewrite rule. // Rule describes a rewrite rule.
type Rule interface { type Rule interface {
// Rewrite rewrites the current request. // 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 returns the processing mode stop or continue.
Mode() string Mode() string
// GetResponseRule returns the rule to rewrite response with, if any. // GetResponseRule returns the rule to rewrite response with, if any.

View File

@@ -7,8 +7,10 @@ import (
"testing" "testing"
"github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -434,12 +436,32 @@ func optsEqual(a, b []dns.EDNS0) bool {
return true 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) { func TestRewriteEDNS0LocalVariable(t *testing.T) {
rw := Rewrite{ rw := Rewrite{
Next: plugin.HandlerFunc(msgPrinter), Next: plugin.HandlerFunc(msgPrinter),
noRevert: true, 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: // test.ResponseWriter has the following values:
// The remote will always be 10.240.0.1 and port 40212. // The remote will always be 10.240.0.1 and port 40212.
// The local address is always 127.0.0.1 and port 53. // 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}}}, []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x7F, 0x00, 0x00, 0x01}}},
true, 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 { for i, tc := range tests {
m := new(dns.Msg) m := new(dns.Msg)
m.SetQuestion("example.com.", dns.TypeA) m.SetQuestion("example.com.", dns.TypeA)
@@ -506,16 +545,19 @@ func TestRewriteEDNS0LocalVariable(t *testing.T) {
} }
rw.Rules = []Rule{r} rw.Rules = []Rule{r}
ctx := context.TODO()
rec := dnstest.NewRecorder(&test.ResponseWriter{}) rec := dnstest.NewRecorder(&test.ResponseWriter{})
rw.ServeDNS(ctx, rec, m) meta.ServeDNS(ctx, rec, m)
resp := rec.Msg resp := rec.Msg
o := resp.IsEdns0() o := resp.IsEdns0()
o.SetDo(tc.doBool)
if o == nil { 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 continue
} }
o.SetDo(tc.doBool)
if o.Do() != tc.doBool { if o.Do() != tc.doBool {
t.Errorf("Test %d: Expected %v but got %v", i, tc.doBool, o.Do()) t.Errorf("Test %d: Expected %v but got %v", i, tc.doBool, o.Do())
} }

View File

@@ -2,6 +2,7 @@
package rewrite package rewrite
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
@@ -30,7 +31,7 @@ func newTypeRule(nextAction string, args ...string) (Rule, error) {
} }
// Rewrite rewrites the the current request. // 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 rule.fromType > 0 && rule.toType > 0 {
if state.QType() == rule.fromType { if state.QType() == rule.fromType {
state.Req.Question[0].Qtype = rule.toType state.Req.Question[0].Qtype = rule.toType