mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-31 02:03:20 -04:00 
			
		
		
		
	feature: plugin/rewrite: rewrite ANSWER SECTION (#1318)
Resolves: #1313
This commit is contained in:
		
				
					committed by
					
						 John Belamaric
						John Belamaric
					
				
			
			
				
	
			
			
			
						parent
						
							cb3190bab1
						
					
				
				
					commit
					258c163bb0
				
			| @@ -41,3 +41,8 @@ func (rule *classRule) Rewrite(w dns.ResponseWriter, r *dns.Msg) Result { | |||||||
| func (rule *classRule) Mode() string { | func (rule *classRule) Mode() string { | ||||||
| 	return rule.NextAction | 	return rule.NextAction | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *classRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -78,6 +78,11 @@ func (rule *edns0NsidRule) Mode() string { | |||||||
| 	return rule.mode | 	return rule.mode | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | 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(w dns.ResponseWriter, r *dns.Msg) Result { | func (rule *edns0LocalRule) Rewrite(w dns.ResponseWriter, r *dns.Msg) Result { | ||||||
| 	result := RewriteIgnored | 	result := RewriteIgnored | ||||||
| @@ -115,6 +120,11 @@ func (rule *edns0LocalRule) Mode() string { | |||||||
| 	return rule.mode | 	return rule.mode | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *edns0LocalRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|  |  | ||||||
| // newEdns0Rule creates an EDNS0 rule of the appropriate type based on the args | // newEdns0Rule creates an EDNS0 rule of the appropriate type based on the args | ||||||
| func newEdns0Rule(mode string, args ...string) (Rule, error) { | func newEdns0Rule(mode string, args ...string) (Rule, error) { | ||||||
| 	if len(args) < 2 { | 	if len(args) < 2 { | ||||||
| @@ -312,6 +322,11 @@ func (rule *edns0VariableRule) Mode() string { | |||||||
| 	return rule.mode | 	return rule.mode | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *edns0VariableRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|  |  | ||||||
| func isValidVariable(variable string) bool { | func isValidVariable(variable string) bool { | ||||||
| 	switch variable { | 	switch variable { | ||||||
| 	case | 	case | ||||||
| @@ -423,6 +438,11 @@ func (rule *edns0SubnetRule) Mode() string { | |||||||
| 	return rule.mode | 	return rule.mode | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *edns0SubnetRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|  |  | ||||||
| // These are all defined actions. | // These are all defined actions. | ||||||
| const ( | const ( | ||||||
| 	Replace = "replace" | 	Replace = "replace" | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ type regexNameRule struct { | |||||||
| 	NextAction  string | 	NextAction  string | ||||||
| 	Pattern     *regexp.Regexp | 	Pattern     *regexp.Regexp | ||||||
| 	Replacement string | 	Replacement string | ||||||
|  | 	ResponseRule | ||||||
| } | } | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -113,9 +114,6 @@ func newNameRule(nextAction string, args ...string) (Rule, error) { | |||||||
| 	if len(args) < 2 { | 	if len(args) < 2 { | ||||||
| 		return nil, fmt.Errorf("too few arguments for a name rule") | 		return nil, fmt.Errorf("too few arguments for a name rule") | ||||||
| 	} | 	} | ||||||
| 	if len(args) > 3 { |  | ||||||
| 		return nil, fmt.Errorf("exceeded the number of arguments for a name rule") |  | ||||||
| 	} |  | ||||||
| 	if len(args) == 3 { | 	if len(args) == 3 { | ||||||
| 		switch strings.ToLower(args[0]) { | 		switch strings.ToLower(args[0]) { | ||||||
| 		case ExactMatch: | 		case ExactMatch: | ||||||
| @@ -131,11 +129,45 @@ func newNameRule(nextAction string, args ...string) (Rule, error) { | |||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return nil, fmt.Errorf("Invalid regex pattern in a name rule: %s", args[1]) | 				return nil, fmt.Errorf("Invalid regex pattern in a name rule: %s", args[1]) | ||||||
| 			} | 			} | ||||||
| 			return ®exNameRule{nextAction, regexPattern, plugin.Name(args[2]).Normalize()}, nil | 			return ®exNameRule{nextAction, regexPattern, plugin.Name(args[2]).Normalize(), ResponseRule{}}, nil | ||||||
| 		default: | 		default: | ||||||
| 			return nil, fmt.Errorf("A name rule supports only exact, prefix, suffix, substring, and regex name matching") | 			return nil, fmt.Errorf("A name rule supports only exact, prefix, suffix, substring, and regex name matching") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	if len(args) == 7 { | ||||||
|  | 		if strings.ToLower(args[0]) == RegexMatch { | ||||||
|  | 			if args[3] != "answer" { | ||||||
|  | 				return nil, fmt.Errorf("exceeded the number of arguments for a regex name rule") | ||||||
|  | 			} | ||||||
|  | 			switch strings.ToLower(args[4]) { | ||||||
|  | 			case "name": | ||||||
|  | 			default: | ||||||
|  | 				return nil, fmt.Errorf("exceeded the number of arguments for a regex name rule") | ||||||
|  | 			} | ||||||
|  | 			regexPattern, err := regexp.Compile(args[1]) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("Invalid regex pattern in a name rule: %s", args) | ||||||
|  | 			} | ||||||
|  | 			responseRegexPattern, err := regexp.Compile(args[5]) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("Invalid regex pattern in a name rule: %s", args) | ||||||
|  | 			} | ||||||
|  | 			return ®exNameRule{ | ||||||
|  | 				nextAction, | ||||||
|  | 				regexPattern, | ||||||
|  | 				plugin.Name(args[2]).Normalize(), | ||||||
|  | 				ResponseRule{ | ||||||
|  | 					Active:      true, | ||||||
|  | 					Pattern:     responseRegexPattern, | ||||||
|  | 					Replacement: plugin.Name(args[6]).Normalize(), | ||||||
|  | 				}, | ||||||
|  | 			}, nil | ||||||
|  | 		} | ||||||
|  | 		return nil, fmt.Errorf("the rewrite of response is supported only for name regex rule") | ||||||
|  | 	} | ||||||
|  | 	if len(args) > 3 && len(args) != 7 { | ||||||
|  | 		return nil, fmt.Errorf("exceeded the number of arguments for a name rule") | ||||||
|  | 	} | ||||||
| 	return &nameRule{nextAction, plugin.Name(args[0]).Normalize(), plugin.Name(args[1]).Normalize()}, nil | 	return &nameRule{nextAction, plugin.Name(args[0]).Normalize(), plugin.Name(args[1]).Normalize()}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -159,3 +191,28 @@ func (rule *substringNameRule) Mode() string { | |||||||
| func (rule *regexNameRule) Mode() string { | func (rule *regexNameRule) Mode() string { | ||||||
| 	return rule.NextAction | 	return rule.NextAction | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *nameRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *prefixNameRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *suffixNameRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *substringNameRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. | ||||||
|  | func (rule *regexNameRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return rule.ResponseRule | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,27 +1,61 @@ | |||||||
| package rewrite | package rewrite | ||||||
|  |  | ||||||
| import "github.com/miekg/dns" | import ( | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ResponseRule contains a rule to rewrite a response with. | ||||||
|  | type ResponseRule struct { | ||||||
|  | 	Active      bool | ||||||
|  | 	Pattern     *regexp.Regexp | ||||||
|  | 	Replacement string | ||||||
|  | } | ||||||
|  |  | ||||||
| // ResponseReverter reverses the operations done on the question section of a packet. | // ResponseReverter reverses the operations done on the question section of a packet. | ||||||
| // This is need because the client will otherwise disregards the response, i.e. | // This is need because the client will otherwise disregards the response, i.e. | ||||||
| // dig will complain with ';; Question section mismatch: got miek.nl/HINFO/IN' | // dig will complain with ';; Question section mismatch: got miek.nl/HINFO/IN' | ||||||
| type ResponseReverter struct { | type ResponseReverter struct { | ||||||
| 	dns.ResponseWriter | 	dns.ResponseWriter | ||||||
| 	original dns.Question | 	originalQuestion dns.Question | ||||||
|  | 	ResponseRewrite  bool | ||||||
|  | 	ResponseRules    []ResponseRule | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewResponseReverter returns a pointer to a new ResponseReverter. | // NewResponseReverter returns a pointer to a new ResponseReverter. | ||||||
| func NewResponseReverter(w dns.ResponseWriter, r *dns.Msg) *ResponseReverter { | func NewResponseReverter(w dns.ResponseWriter, r *dns.Msg) *ResponseReverter { | ||||||
| 	return &ResponseReverter{ | 	return &ResponseReverter{ | ||||||
| 		ResponseWriter:   w, | 		ResponseWriter:   w, | ||||||
| 		original:       r.Question[0], | 		originalQuestion: r.Question[0], | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // WriteMsg records the status code and calls the | // WriteMsg records the status code and calls the | ||||||
| // underlying ResponseWriter's WriteMsg method. | // underlying ResponseWriter's WriteMsg method. | ||||||
| func (r *ResponseReverter) WriteMsg(res *dns.Msg) error { | func (r *ResponseReverter) WriteMsg(res *dns.Msg) error { | ||||||
| 	res.Question[0] = r.original | 	res.Question[0] = r.originalQuestion | ||||||
|  | 	if r.ResponseRewrite { | ||||||
|  | 		for _, rr := range res.Answer { | ||||||
|  | 			name := rr.(*dns.A).Hdr.Name | ||||||
|  | 			for _, rule := range r.ResponseRules { | ||||||
|  | 				regexGroups := rule.Pattern.FindStringSubmatch(name) | ||||||
|  | 				if len(regexGroups) == 0 { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				s := rule.Replacement | ||||||
|  | 				for groupIndex, groupValue := range regexGroups { | ||||||
|  | 					groupIndexStr := "{" + strconv.Itoa(groupIndex) + "}" | ||||||
|  | 					if strings.Contains(s, groupIndexStr) { | ||||||
|  | 						s = strings.Replace(s, groupIndexStr, groupValue, -1) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				name = s | ||||||
|  | 			} | ||||||
|  | 			rr.(*dns.A).Hdr.Name = name | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return r.ResponseWriter.WriteMsg(res) | 	return r.ResponseWriter.WriteMsg(res) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -45,6 +45,11 @@ func (rw Rewrite) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg | |||||||
| 	for _, rule := range rw.Rules { | 	for _, rule := range rw.Rules { | ||||||
| 		switch result := rule.Rewrite(w, r); result { | 		switch result := rule.Rewrite(w, r); result { | ||||||
| 		case RewriteDone: | 		case RewriteDone: | ||||||
|  | 			respRule := rule.GetResponseRule() | ||||||
|  | 			if respRule.Active == true { | ||||||
|  | 				wr.ResponseRewrite = true | ||||||
|  | 				wr.ResponseRules = append(wr.ResponseRules, respRule) | ||||||
|  | 			} | ||||||
| 			if rule.Mode() == Stop { | 			if rule.Mode() == Stop { | ||||||
| 				if rw.noRevert { | 				if rw.noRevert { | ||||||
| 					return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, w, r) | 					return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, w, r) | ||||||
| @@ -70,8 +75,10 @@ func (rw Rewrite) Name() string { return "rewrite" } | |||||||
| type Rule interface { | type Rule interface { | ||||||
| 	// Rewrite rewrites the current request. | 	// Rewrite rewrites the current request. | ||||||
| 	Rewrite(dns.ResponseWriter, *dns.Msg) Result | 	Rewrite(dns.ResponseWriter, *dns.Msg) 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() ResponseRule | ||||||
| } | } | ||||||
|  |  | ||||||
| func newRule(args ...string) (Rule, error) { | func newRule(args ...string) (Rule, error) { | ||||||
|   | |||||||
| @@ -36,6 +36,13 @@ func TestNewRule(t *testing.T) { | |||||||
| 		{[]string{"name", "substring", "a.com", "b.com"}, false, reflect.TypeOf(&substringNameRule{})}, | 		{[]string{"name", "substring", "a.com", "b.com"}, false, reflect.TypeOf(&substringNameRule{})}, | ||||||
| 		{[]string{"name", "regex", "([a])\\.com", "new-{1}.com"}, false, reflect.TypeOf(®exNameRule{})}, | 		{[]string{"name", "regex", "([a])\\.com", "new-{1}.com"}, false, reflect.TypeOf(®exNameRule{})}, | ||||||
| 		{[]string{"name", "regex", "([a]\\.com", "new-{1}.com"}, true, nil}, | 		{[]string{"name", "regex", "([a]\\.com", "new-{1}.com"}, true, nil}, | ||||||
|  | 		{[]string{"name", "regex", "(dns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "name", "(core)\\.(dns)\\.(rocks)", "{2}.{1}.{3}"}, false, reflect.TypeOf(®exNameRule{})}, | ||||||
|  | 		{[]string{"name", "regex", "(adns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "name", "(core)\\.(adns)\\.(rocks)", "{2}.{1}.{3}", "too.long", "way.too.long"}, true, nil}, | ||||||
|  | 		{[]string{"name", "regex", "(bdns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "NoAnswer", "name", "(core)\\.(bdns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, | ||||||
|  | 		{[]string{"name", "regex", "(cdns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "ttl", "(core)\\.(cdns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, | ||||||
|  | 		{[]string{"name", "regex", "(ddns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "name", "\xecore\\.(ddns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, | ||||||
|  | 		{[]string{"name", "regex", "\xedns\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "name", "(core)\\.(edns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, | ||||||
|  | 		{[]string{"name", "substring", "fcore.dns.rocks", "dns.fcore.rocks", "answer", "name", "(fcore)\\.(dns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, | ||||||
| 		{[]string{"name", "substring", "a.com", "b.com", "c.com"}, true, nil}, | 		{[]string{"name", "substring", "a.com", "b.com", "c.com"}, true, nil}, | ||||||
| 		{[]string{"type"}, true, nil}, | 		{[]string{"type"}, true, nil}, | ||||||
| 		{[]string{"type", "a"}, true, nil}, | 		{[]string{"type", "a"}, true, nil}, | ||||||
| @@ -152,6 +159,8 @@ func TestRewrite(t *testing.T) { | |||||||
| 	rules := []Rule{} | 	rules := []Rule{} | ||||||
| 	r, _ := newNameRule("stop", "from.nl.", "to.nl.") | 	r, _ := newNameRule("stop", "from.nl.", "to.nl.") | ||||||
| 	rules = append(rules, r) | 	rules = append(rules, r) | ||||||
|  | 	r, _ = newNameRule("stop", "regex", "(core)\\.(dns)\\.(rocks)\\.(nl)", "{2}.{1}.{3}.{4}", "answer", "name", "(dns)\\.(core)\\.(rocks)\\.(nl)", "{2}.{1}.{3}.{4}") | ||||||
|  | 	rules = append(rules, r) | ||||||
| 	r, _ = newNameRule("stop", "exact", "from.exact.nl.", "to.nl.") | 	r, _ = newNameRule("stop", "exact", "from.exact.nl.", "to.nl.") | ||||||
| 	rules = append(rules, r) | 	rules = append(rules, r) | ||||||
| 	r, _ = newNameRule("stop", "prefix", "prefix", "to") | 	r, _ = newNameRule("stop", "prefix", "prefix", "to") | ||||||
| @@ -203,6 +212,7 @@ func TestRewrite(t *testing.T) { | |||||||
| 		{"a.nl.", dns.TypeANY, dns.ClassCHAOS, "a.nl.", dns.TypeANY, dns.ClassINET}, | 		{"a.nl.", dns.TypeANY, dns.ClassCHAOS, "a.nl.", dns.TypeANY, dns.ClassINET}, | ||||||
| 		// class gets rewritten twice because of continue/stop logic: HS to CH, CH to IN | 		// class gets rewritten twice because of continue/stop logic: HS to CH, CH to IN | ||||||
| 		{"a.nl.", dns.TypeANY, 4, "a.nl.", dns.TypeANY, dns.ClassINET}, | 		{"a.nl.", dns.TypeANY, 4, "a.nl.", dns.TypeANY, dns.ClassINET}, | ||||||
|  | 		{"core.dns.rocks.nl.", dns.TypeA, dns.ClassINET, "dns.core.rocks.nl.", dns.TypeA, dns.ClassINET}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx := context.TODO() | 	ctx := context.TODO() | ||||||
| @@ -224,6 +234,13 @@ func TestRewrite(t *testing.T) { | |||||||
| 		if resp.Question[0].Qclass != tc.toC { | 		if resp.Question[0].Qclass != tc.toC { | ||||||
| 			t.Errorf("Test %d: Expected Class to be '%d' but was '%d'", i, tc.toC, resp.Question[0].Qclass) | 			t.Errorf("Test %d: Expected Class to be '%d' but was '%d'", i, tc.toC, resp.Question[0].Qclass) | ||||||
| 		} | 		} | ||||||
|  | 		if tc.fromT == dns.TypeA && tc.toT == dns.TypeA { | ||||||
|  | 			if len(resp.Answer) > 0 { | ||||||
|  | 				if resp.Answer[0].(*dns.A).Hdr.Name != tc.to { | ||||||
|  | 					t.Errorf("Test %d: Expected Answer Name to be %q but was %q", i, tc.to, resp.Answer[0].(*dns.A).Hdr.Name) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ package rewrite | |||||||
| import ( | import ( | ||||||
| 	"github.com/coredns/coredns/core/dnsserver" | 	"github.com/coredns/coredns/core/dnsserver" | ||||||
| 	"github.com/coredns/coredns/plugin" | 	"github.com/coredns/coredns/plugin" | ||||||
|  |  | ||||||
| 	"github.com/mholt/caddy" | 	"github.com/mholt/caddy" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -32,6 +31,12 @@ func rewriteParse(c *caddy.Controller) ([]Rule, error) { | |||||||
|  |  | ||||||
| 	for c.Next() { | 	for c.Next() { | ||||||
| 		args := c.RemainingArgs() | 		args := c.RemainingArgs() | ||||||
|  | 		if len(args) < 2 { | ||||||
|  | 			// Handles rules out of nested instructions, i.e. the ones enclosed in curly brackets | ||||||
|  | 			for c.NextBlock() { | ||||||
|  | 				args = append(args, c.Val()) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 		rule, err := newRule(args...) | 		rule, err := newRule(args...) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
|   | |||||||
| @@ -42,3 +42,8 @@ func (rule *typeRule) Rewrite(w dns.ResponseWriter, r *dns.Msg) Result { | |||||||
| func (rule *typeRule) Mode() string { | func (rule *typeRule) Mode() string { | ||||||
| 	return rule.nextAction | 	return rule.nextAction | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetResponseRule return a rule to rewrite the response with. Currently not implemented. | ||||||
|  | func (rule *typeRule) GetResponseRule() ResponseRule { | ||||||
|  | 	return ResponseRule{} | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user