mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-27 08:14:18 -04:00 
			
		
		
		
	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:
		
				
					committed by
					
						 Miek Gieben
						Miek Gieben
					
				
			
			
				
	
			
			
			
						parent
						
							6ec1978340
						
					
				
				
					commit
					7745462430
				
			| @@ -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 | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| 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 { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| 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 { | ||||
| 			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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user