diff options
-rw-r--r-- | plugin/rewrite/README.md | 99 | ||||
-rw-r--r-- | plugin/rewrite/class.go | 9 | ||||
-rw-r--r-- | plugin/rewrite/edns0.go | 50 | ||||
-rw-r--r-- | plugin/rewrite/name.go | 502 | ||||
-rw-r--r-- | plugin/rewrite/name_test.go | 263 | ||||
-rw-r--r-- | plugin/rewrite/reverter.go | 135 | ||||
-rw-r--r-- | plugin/rewrite/reverter_test.go | 86 | ||||
-rw-r--r-- | plugin/rewrite/rewrite.go | 34 | ||||
-rw-r--r-- | plugin/rewrite/rewrite_test.go | 32 | ||||
-rw-r--r-- | plugin/rewrite/setup_test.go | 4 | ||||
-rw-r--r-- | plugin/rewrite/ttl.go | 168 | ||||
-rw-r--r-- | plugin/rewrite/ttl_test.go | 5 | ||||
-rw-r--r-- | plugin/rewrite/type.go | 9 |
13 files changed, 882 insertions, 514 deletions
diff --git a/plugin/rewrite/README.md b/plugin/rewrite/README.md index 1665d658a..b0757f68b 100644 --- a/plugin/rewrite/README.md +++ b/plugin/rewrite/README.md @@ -13,23 +13,36 @@ Rewrites are invisible to the client. There are simple rewrites (fast) and compl A simplified/easy-to-digest syntax for *rewrite* is... ~~~ -rewrite [continue|stop] FIELD [FROM TO|FROM TTL] +rewrite [continue|stop] FIELD [TYPE] [(FROM TO)|TTL] [OPTIONS] ~~~ * **FIELD** indicates what part of the request/response is being re-written. * `type` - the type field of the request will be rewritten. FROM/TO must be a DNS record type (`A`, `MX`, etc.); e.g., to rewrite ANY queries to HINFO, use `rewrite type ANY HINFO`. - * `class` - the class of the message will be rewritten. FROM/TO must be a DNS class type (`IN`, `CH`, or `HS`); e.g., to rewrite CH queries to IN use `rewrite class CH IN`. * `name` - the query name in the _request_ is rewritten; by default this is a full match of the name, e.g., `rewrite name example.net example.org`. Other match types are supported, see the **Name Field Rewrites** section below. - * `answer name` - the query name in the _response_ is rewritten. This option has special restrictions and requirements, in particular it must always combined with a `name` rewrite. See below in the **Response Rewrites** section. - * `edns0` - an EDNS0 option can be appended to the request as described below in the **EDNS0 Options** section. + * `class` - the class of the message will be rewritten. FROM/TO must be a DNS class type (`IN`, `CH`, or `HS`); e.g., to rewrite CH queries to IN use `rewrite class CH IN`. + * `edns0` - an EDNS0 option can be appended to the request as described below in the **EDNS0 Options** section. * `ttl` - the TTL value in the _response_ is rewritten. +* **TYPE** this optional element can be specified for a `name` or `ttl` field. + If not given type `exact` will be assumed. If options should be specified the + type must be given. * **FROM** is the name (exact, suffix, prefix, substring, or regex) or type to match * **TO** is the destination name or type to rewrite to -* **TTL** is the number of seconds to set the TTL value to +* **TTL** is the number of seconds to set the TTL value to (only for field `ttl`) + +* **OPTIONS** + + for field `name` further options are possible controlling the response rewrites. + All name matching types support the following options + + * `answer auto` - the names in the _response_ is rewritten in a best effort manner. + * `answer name FROM TO` - the query name in the _response_ is rewritten matching the from regex pattern. + * `answer value FROM TO` - the names in the _response_ is rewritten matching the from regex pattern. + + See below in the **Response Rewrites** section for further details. If you specify multiple rules and an incoming query matches multiple rules, the rewrite will behave as follows: @@ -49,7 +62,7 @@ client. The syntax for name rewriting is as follows: ``` -rewrite [continue|stop] name [exact|prefix|suffix|substring|regex] STRING STRING +rewrite [continue|stop] name [exact|prefix|suffix|substring|regex] STRING STRING [OPTIONS] ``` The match type, e.g., `exact`, `substring`, etc., triggers rewrite: @@ -60,7 +73,8 @@ The match type, e.g., `exact`, `substring`, etc., triggers rewrite: * **suffix**: when the name ends with the matching string * **regex**: when the name in the question section of a request matches a regular expression -If the match type is omitted, the `exact` match type is assumed. +If the match type is omitted, the `exact` match type is assumed. If OPTIONS are +given, the type must be specified. The following instruction allows rewriting names in the query that contain the substring `service.us-west-1.example.org`: @@ -95,7 +109,8 @@ rewrite name suffix .schmoogle.com. .google.com. ### Response Rewrites -When rewriting incoming DNS requests' names, CoreDNS re-writes the `QUESTION SECTION` +When rewriting incoming DNS requests' names (field `name`), CoreDNS re-writes +the `QUESTION SECTION` section of the requests. It may be necessary to rewrite the `ANSWER SECTION` of the requests, because some DNS resolvers treat mismatches between the `QUESTION SECTION` and `ANSWER SECTION` as a man-in-the-middle attack (MITM). @@ -127,6 +142,35 @@ ftp.service.us-west-1.consul. 0 IN A 10.30.30.30 The above is a mismatch between the question asked and the answer provided. +There are three possibilities to specify an answer rewrite: +- A rewrite can request a best effort answer rewrite by adding the option `answer auto`. +- A rewrite may specify a dedicated regex based response name rewrite with the + `answer name FROM TO` option. +- A regex based rewrite of record values like `CNAME`, `SRV`, etc, can be requested by + an `answer value FROM TO` option. + +Hereby FROM/TO follow the rules for the `regex` name rewrite syntax. + +#### Auto Response Name Rewrite + +The following configuration snippet allows for rewriting of the +`ANSWER SECTION` according to the rewrite of the `QUESTION SECTION`: + +``` + rewrite stop { + name suffix .coredns.rocks .service.consul answer auto + } +``` + +Any occurrence of the rewritten question in the answer is mapped +back to the original value before the rewrite. + +Please note that answers for rewrites of type `exact` are always rewritten. +For a `suffix` name rule `auto` leads to a reverse suffix response rewrite, +exchanging FROM and TO from the rewrite request. + +#### Explicit Response Name Rewrite + The following configuration snippet allows for rewriting of the `ANSWER SECTION`, provided that the `QUESTION SECTION` was rewritten: @@ -151,9 +195,11 @@ ftp-us-west-1.coredns.rocks. 0 IN A 10.20.20.20 ftp-us-west-1.coredns.rocks. 0 IN A 10.30.30.30 ``` +#### Rewriting other Response Values + It is also possible to rewrite other values returned in the DNS response records (e.g. the server names returned in `SRV` and `MX` records). This can be enabled by adding -the `answer value` to a name regex rule as specified below. `answer value` takes a +the `answer value FROM TO` option to a name rule as specified below. `answer value` takes a regular expression and a rewrite name as parameters and works in the same way as the `answer name` rule. @@ -173,20 +219,29 @@ rewrite [continue|stop] { ``` Note that the above syntax is strict. For response rewrites, only `name` -rules are allowed to match the question section, and only by match type -`regex`. The answer rewrite must be after the name, as in the -syntax example. +rules are allowed to match the question section. The answer rewrite must be +after the name, as in the syntax example. + +#### Multiple Response Rewrites + +`name` and `value` rewrites can be chained by appending multiple answer rewrite +options. For all occurrences but the first one the keyword `answer` might be +omitted. -An alternate syntax for rewriting a DNS request and response is as -follows: +```options +answer (auto | (name|value FROM TO)) { [answer] (auto | (name|value FROM TO)) } +``` +For example: ``` -rewrite [continue|stop] name regex STRING STRING answer name STRING STRING [answer value STRING STRING] +rewrite [continue|stop] name regex FROM TO answer name FROM TO [answer] value FROM TO ``` When using `exact` name rewrite rules, the answer gets rewritten automatically, -and there is no need to define `answer name`. The rule below -rewrites the name in a request from `RED` to `BLUE`, and subsequently +and there is no need to define `answer name auto`. But it is still possible to define +additional `answer value` and `answer value` options. + +The rule below rewrites the name in a request from `RED` to `BLUE`, and subsequently rewrites the name in a corresponding response from `BLUE` to `RED`. The client in the request would see only `RED` and no `BLUE`. @@ -215,6 +270,7 @@ setting the TTL value really low. The syntax for the TTL rewrite rule is as follows. The meaning of `exact|prefix|suffix|substring|regex` is the same as with the name rewrite rules. +An omitted type is defaulted to `exact`. ``` rewrite [continue|stop] ttl [exact|prefix|suffix|substring|regex] STRING SECONDS @@ -290,12 +346,3 @@ rewrite edns0 subnet set 24 56 * If the query's source IP address is an IPv4 address, the first 24 bits in the IP will be the network subnet. * If the query's source IP address is an IPv6 address, the first 56 bits in the IP will be the network subnet. - -## Full Syntax - -The full plugin usage syntax is harder to digest... -~~~ -rewrite [continue|stop] {type|class|edns0|name [exact|prefix|suffix|substring|regex [FROM TO answer name]]} FROM TO -~~~ - -The syntax above doesn't cover the multi-line block option for specifying a name request+response rewrite rule described in the **Response Rewrite** section. diff --git a/plugin/rewrite/class.go b/plugin/rewrite/class.go index 178cf8181..243a86449 100644 --- a/plugin/rewrite/class.go +++ b/plugin/rewrite/class.go @@ -30,18 +30,15 @@ func newClassRule(nextAction string, args ...string) (Rule, error) { } // Rewrite rewrites the current request. -func (rule *classRule) Rewrite(ctx context.Context, state request.Request) Result { +func (rule *classRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { if rule.fromClass > 0 && rule.toClass > 0 { if state.Req.Question[0].Qclass == rule.fromClass { state.Req.Question[0].Qclass = rule.toClass - return RewriteDone + return nil, RewriteDone } } - return RewriteIgnored + return nil, RewriteIgnored } // Mode returns the processing mode. func (rule *classRule) Mode() string { return rule.NextAction } - -// GetResponseRules return rules to rewrite the response with. Currently not implemented. -func (rule *classRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } diff --git a/plugin/rewrite/edns0.go b/plugin/rewrite/edns0.go index fd355d678..5e3663ef6 100644 --- a/plugin/rewrite/edns0.go +++ b/plugin/rewrite/edns0.go @@ -49,14 +49,14 @@ func setupEdns0Opt(r *dns.Msg) *dns.OPT { } // Rewrite will alter the request EDNS0 NSID option -func (rule *edns0NsidRule) Rewrite(ctx context.Context, state request.Request) Result { +func (rule *edns0NsidRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { o := setupEdns0Opt(state.Req) for _, s := range o.Option { if e, ok := s.(*dns.EDNS0_NSID); ok { if rule.action == Replace || rule.action == Set { e.Nsid = "" // make sure it is empty for request - return RewriteDone + return nil, RewriteDone } } } @@ -64,20 +64,17 @@ func (rule *edns0NsidRule) Rewrite(ctx context.Context, state request.Request) R // add option if not found if rule.action == Append || rule.action == Set { o.Option = append(o.Option, &dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}) - return RewriteDone + return nil, RewriteDone } - return RewriteIgnored + return nil, RewriteIgnored } // Mode returns the processing mode. func (rule *edns0NsidRule) Mode() string { return rule.mode } -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *edns0NsidRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } - // Rewrite will alter the request EDNS0 local options. -func (rule *edns0LocalRule) Rewrite(ctx context.Context, state request.Request) Result { +func (rule *edns0LocalRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { o := setupEdns0Opt(state.Req) for _, s := range o.Option { @@ -85,7 +82,7 @@ func (rule *edns0LocalRule) Rewrite(ctx context.Context, state request.Request) if rule.code == e.Code { if rule.action == Replace || rule.action == Set { e.Data = rule.data - return RewriteDone + return nil, RewriteDone } } } @@ -94,18 +91,15 @@ func (rule *edns0LocalRule) Rewrite(ctx context.Context, state request.Request) // add option if not found if rule.action == Append || rule.action == Set { o.Option = append(o.Option, &dns.EDNS0_LOCAL{Code: rule.code, Data: rule.data}) - return RewriteDone + return nil, RewriteDone } - return RewriteIgnored + return nil, RewriteIgnored } // Mode returns the processing mode. func (rule *edns0LocalRule) Mode() string { return rule.mode } -// GetResponseRules returns a rule to rewrite the response with. Currently not implemented. -func (rule *edns0LocalRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } - // newEdns0Rule creates an EDNS0 rule of the appropriate type based on the args func newEdns0Rule(mode string, args ...string) (Rule, error) { if len(args) < 2 { @@ -222,10 +216,10 @@ func (rule *edns0VariableRule) ruleData(ctx context.Context, state request.Reque } // Rewrite will alter the request EDNS0 local options with specified variables. -func (rule *edns0VariableRule) Rewrite(ctx context.Context, state request.Request) Result { +func (rule *edns0VariableRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { data, err := rule.ruleData(ctx, state) if err != nil || data == nil { - return RewriteIgnored + return nil, RewriteIgnored } o := setupEdns0Opt(state.Req) @@ -234,9 +228,9 @@ func (rule *edns0VariableRule) Rewrite(ctx context.Context, state request.Reques if rule.code == e.Code { if rule.action == Replace || rule.action == Set { e.Data = data - return RewriteDone + return nil, RewriteDone } - return RewriteIgnored + return nil, RewriteIgnored } } } @@ -244,18 +238,15 @@ func (rule *edns0VariableRule) Rewrite(ctx context.Context, state request.Reques // add option if not found if rule.action == Append || rule.action == Set { o.Option = append(o.Option, &dns.EDNS0_LOCAL{Code: rule.code, Data: data}) - return RewriteDone + return nil, RewriteDone } - return RewriteIgnored + return nil, RewriteIgnored } // Mode returns the processing mode. func (rule *edns0VariableRule) Mode() string { return rule.mode } -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *edns0VariableRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } - func isValidVariable(variable string) bool { switch variable { case @@ -333,17 +324,17 @@ func (rule *edns0SubnetRule) fillEcsData(state request.Request, ecs *dns.EDNS0_S } // Rewrite will alter the request EDNS0 subnet option. -func (rule *edns0SubnetRule) Rewrite(ctx context.Context, state request.Request) Result { +func (rule *edns0SubnetRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { o := setupEdns0Opt(state.Req) for _, s := range o.Option { if e, ok := s.(*dns.EDNS0_SUBNET); ok { if rule.action == Replace || rule.action == Set { if rule.fillEcsData(state, e) == nil { - return RewriteDone + return nil, RewriteDone } } - return RewriteIgnored + return nil, RewriteIgnored } } @@ -352,19 +343,16 @@ func (rule *edns0SubnetRule) Rewrite(ctx context.Context, state request.Request) opt := &dns.EDNS0_SUBNET{Code: dns.EDNS0SUBNET} if rule.fillEcsData(state, opt) == nil { o.Option = append(o.Option, opt) - return RewriteDone + return nil, RewriteDone } } - return RewriteIgnored + return nil, RewriteIgnored } // Mode returns the processing mode func (rule *edns0SubnetRule) Mode() string { return rule.mode } -// GetResponseRules return rules to rewrite the response with. Currently not implemented. -func (rule *edns0SubnetRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } - // These are all defined actions. const ( Replace = "replace" diff --git a/plugin/rewrite/name.go b/plugin/rewrite/name.go index 24c813983..e9612839f 100644 --- a/plugin/rewrite/name.go +++ b/plugin/rewrite/name.go @@ -9,38 +9,106 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/request" + + "github.com/miekg/dns" ) -type exactNameRule struct { - NextAction string - From string - To string - ResponseRule +// stringRewriter rewrites a string +type stringRewriter interface { + rewriteString(src string) string } -type prefixNameRule struct { - NextAction string - Prefix string - Replacement string +// regexStringRewriter can be used to rewrite strings by regex pattern. +// it contains all the information required to detect and execute a rewrite +// on a string. +type regexStringRewriter struct { + pattern *regexp.Regexp + replacement string } -type suffixNameRule struct { - NextAction string - Suffix string - Replacement string +var _ stringRewriter = ®exStringRewriter{} + +func newStringRewriter(pattern *regexp.Regexp, replacement string) stringRewriter { + return ®exStringRewriter{pattern, replacement} } -type substringNameRule struct { - NextAction string - Substring string - Replacement string +func (r *regexStringRewriter) rewriteString(src string) string { + regexGroups := r.pattern.FindStringSubmatch(src) + if len(regexGroups) == 0 { + return src + } + s := r.replacement + for groupIndex, groupValue := range regexGroups { + groupIndexStr := "{" + strconv.Itoa(groupIndex) + "}" + s = strings.Replace(s, groupIndexStr, groupValue, -1) + } + return s } -type regexNameRule struct { - NextAction string - Pattern *regexp.Regexp - Replacement string - ResponseRules []ResponseRule +// remapStringRewriter maps a dedicated string to another string +// it also maps a the domain of a sub domain. +type remapStringRewriter struct { + orig string + replacement string +} + +var _ stringRewriter = &remapStringRewriter{} + +func newRemapStringRewriter(orig, replacement string) stringRewriter { + return &remapStringRewriter{orig, replacement} +} + +func (r *remapStringRewriter) rewriteString(src string) string { + if src == r.orig { + return r.replacement + } + if strings.HasSuffix(src, "."+r.orig) { + return src[0:len(src)-len(r.orig)] + r.replacement + } + return src +} + +// suffixStringRewriter maps a dedicated suffix string to another string +type suffixStringRewriter struct { + suffix string + replacement string +} + +var _ stringRewriter = &suffixStringRewriter{} + +func newSuffixStringRewriter(orig, replacement string) stringRewriter { + return &suffixStringRewriter{orig, replacement} +} + +func (r *suffixStringRewriter) rewriteString(src string) string { + if strings.HasSuffix(src, r.suffix) { + return strings.TrimSuffix(src, r.suffix) + r.replacement + } + return src +} + +// nameRewriterResponseRule maps a record name according to a stringRewriter. +type nameRewriterResponseRule struct { + stringRewriter +} + +func (r *nameRewriterResponseRule) RewriteResponse(rr dns.RR) { + rr.Header().Name = r.rewriteString(rr.Header().Name) +} + +// valueRewriterResponseRule maps a record value according to a stringRewriter. +type valueRewriterResponseRule struct { + stringRewriter +} + +func (r *valueRewriterResponseRule) RewriteResponse(rr dns.RR) { + value := getRecordValueForRewrite(rr) + if value != "" { + new := r.rewriteString(value) + if new != value { + setRewrittenRecordValue(rr, new) + } + } } const ( @@ -54,60 +122,173 @@ const ( SubstringMatch = "substring" // RegexMatch matches when the name in the question section of a request matches a regular expression RegexMatch = "regex" + + // AnswerMatch matches an answer rewrite + AnswerMatch = "answer" + // AutoMatch matches the auto name answer rewrite + AutoMatch = "auto" + // NameMatch matches the name answer rewrite + NameMatch = "name" + // ValueMatch matches the value answer rewrite + ValueMatch = "value" ) -// Rewrite rewrites the current request based upon exact match of the name +type nameRuleBase struct { + nextAction string + auto bool + replacement string + static ResponseRules +} + +func newNameRuleBase(nextAction string, auto bool, replacement string, staticResponses ResponseRules) nameRuleBase { + return nameRuleBase{ + nextAction: nextAction, + auto: auto, + replacement: replacement, + static: staticResponses, + } +} + +// responseRuleFor create for auto mode dynamically response rewriters for name and value +// reverting the mapping done by the name rewrite rule, which can be found in the state. +func (rule *nameRuleBase) responseRuleFor(state request.Request) (ResponseRules, Result) { + if !rule.auto { + return rule.static, RewriteDone + } + + rewriter := newRemapStringRewriter(state.Req.Question[0].Name, state.Name()) + rules := ResponseRules{ + &nameRewriterResponseRule{rewriter}, + &valueRewriterResponseRule{rewriter}, + } + return append(rules, rule.static...), RewriteDone +} + +// Mode returns the processing nextAction +func (rule *nameRuleBase) Mode() string { return rule.nextAction } + +// exactNameRule rewrites the current request based upon exact match of the name // in the question section of the request. -func (rule *exactNameRule) Rewrite(ctx context.Context, state request.Request) Result { - if rule.From == state.Name() { - state.Req.Question[0].Name = rule.To - return RewriteDone +type exactNameRule struct { + nameRuleBase + from string +} + +func newExactNameRule(nextAction string, orig, replacement string, answers ResponseRules) Rule { + return &exactNameRule{ + newNameRuleBase(nextAction, true, replacement, answers), + orig, + } +} + +func (rule *exactNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if rule.from == state.Name() { + state.Req.Question[0].Name = rule.replacement + return rule.responseRuleFor(state) } - return RewriteIgnored + return nil, RewriteIgnored +} + +// prefixNameRule rewrites the current request when the name begins with the matching string. +type prefixNameRule struct { + nameRuleBase + prefix string } -// Rewrite rewrites the current request when the name begins with the matching string. -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.TrimPrefix(state.Name(), rule.Prefix) - return RewriteDone +func newPrefixNameRule(nextAction string, auto bool, prefix, replacement string, answers ResponseRules) Rule { + return &prefixNameRule{ + newNameRuleBase(nextAction, auto, replacement, answers), + prefix, } - return RewriteIgnored } -// Rewrite rewrites the current request when the name ends with the matching string. -func (rule *suffixNameRule) Rewrite(ctx context.Context, state request.Request) Result { - if strings.HasSuffix(state.Name(), rule.Suffix) { - state.Req.Question[0].Name = strings.TrimSuffix(state.Name(), rule.Suffix) + rule.Replacement - return RewriteDone +func (rule *prefixNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if strings.HasPrefix(state.Name(), rule.prefix) { + state.Req.Question[0].Name = rule.replacement + strings.TrimPrefix(state.Name(), rule.prefix) + return rule.responseRuleFor(state) } - return RewriteIgnored + return nil, RewriteIgnored } -// Rewrite rewrites the current request based upon partial match of the +// suffixNameRule rewrites the current request when the name ends with the matching string. +type suffixNameRule struct { + nameRuleBase + suffix string +} + +func newSuffixNameRule(nextAction string, auto bool, suffix, replacement string, answers ResponseRules) Rule { + var rules ResponseRules + if auto { + // for a suffix rewriter better standard response rewrites can be done + // just by using the original suffix/replacement in the opposite order + rewriter := newSuffixStringRewriter(replacement, suffix) + rules = ResponseRules{ + &nameRewriterResponseRule{rewriter}, + &valueRewriterResponseRule{rewriter}, + } + } + return &suffixNameRule{ + newNameRuleBase(nextAction, false, replacement, append(rules, answers...)), + suffix, + } +} + +func (rule *suffixNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if strings.HasSuffix(state.Name(), rule.suffix) { + state.Req.Question[0].Name = strings.TrimSuffix(state.Name(), rule.suffix) + rule.replacement + return rule.responseRuleFor(state) + } + return nil, RewriteIgnored +} + +// substringNameRule rewrites the current request based upon partial match of the // name in the question section of the request. -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 +type substringNameRule struct { + nameRuleBase + substring string +} + +func newSubstringNameRule(nextAction string, auto bool, substring, replacement string, answers ResponseRules) Rule { + return &substringNameRule{ + newNameRuleBase(nextAction, auto, replacement, answers), + substring, + } +} + +func (rule *substringNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if strings.Contains(state.Name(), rule.substring) { + state.Req.Question[0].Name = strings.Replace(state.Name(), rule.substring, rule.replacement, -1) + return rule.responseRuleFor(state) } - return RewriteIgnored + return nil, RewriteIgnored } -// Rewrite rewrites the current request when the name in the question +// regexNameRule rewrites the current request when the name in the question // section of the request matches a regular expression. -func (rule *regexNameRule) Rewrite(ctx context.Context, state request.Request) Result { - regexGroups := rule.Pattern.FindStringSubmatch(state.Name()) +type regexNameRule struct { + nameRuleBase + pattern *regexp.Regexp +} + +func newRegexNameRule(nextAction string, auto bool, pattern *regexp.Regexp, replacement string, answers ResponseRules) Rule { + return ®exNameRule{ + newNameRuleBase(nextAction, auto, replacement, answers), + pattern, + } +} + +func (rule *regexNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + regexGroups := rule.pattern.FindStringSubmatch(state.Name()) if len(regexGroups) == 0 { - return RewriteIgnored + return nil, RewriteIgnored } - s := rule.Replacement + s := rule.replacement for groupIndex, groupValue := range regexGroups { groupIndexStr := "{" + strconv.Itoa(groupIndex) + "}" s = strings.Replace(s, groupIndexStr, groupValue, -1) } state.Req.Question[0].Name = s - return RewriteDone + return rule.responseRuleFor(state) } // newNameRule creates a name matching rule based on exact, partial, or regex match @@ -139,149 +320,106 @@ func newNameRule(nextAction string, args ...string) (Rule, error) { } } - //if len(args) > 3 && len(args) != 7 { - if len(args) > 3 && (len(args)-3)%4 != 0 { - return nil, fmt.Errorf("response rewrites must consist only of a name rule with 3 arguments and one or more answer rules with 3 arguments each") + var err error + var answers ResponseRules + auto := false + if len(args) > 3 { + auto, answers, err = parseAnswerRules(matchType, args[3:]) + if err != nil { + return nil, err + } } - if len(args) < 7 { - switch matchType { - case ExactMatch: - rewriteAnswerFromPattern, err := isValidRegexPattern(rewriteQuestionTo, rewriteQuestionFrom) - if err != nil { - return nil, err - } - return &exactNameRule{ - nextAction, - rewriteQuestionFrom, - rewriteQuestionTo, - ResponseRule{ - Active: true, - Type: "name", - Pattern: rewriteAnswerFromPattern, - Replacement: rewriteQuestionFrom, - }, - }, nil - case PrefixMatch: - return &prefixNameRule{ - nextAction, - rewriteQuestionFrom, - rewriteQuestionTo, - }, nil - case SuffixMatch: - return &suffixNameRule{ - nextAction, - rewriteQuestionFrom, - rewriteQuestionTo, - }, nil - case SubstringMatch: - return &substringNameRule{ - nextAction, - rewriteQuestionFrom, - rewriteQuestionTo, - }, nil - case RegexMatch: - rewriteQuestionFromPattern, err := isValidRegexPattern(rewriteQuestionFrom, rewriteQuestionTo) - if err != nil { - return nil, err - } - rewriteQuestionTo := plugin.Name(args[2]).Normalize() - return ®exNameRule{ - nextAction, - rewriteQuestionFromPattern, - rewriteQuestionTo, - []ResponseRule{{ - Type: "name", - }}, - }, nil - default: - return nil, fmt.Errorf("name rule supports only exact, prefix, suffix, substring, and regex name matching, received: %s", matchType) + switch matchType { + case ExactMatch: + if _, err := isValidRegexPattern(rewriteQuestionTo, rewriteQuestionFrom); err != nil { + return nil, err + } + return newExactNameRule(nextAction, rewriteQuestionFrom, rewriteQuestionTo, answers), nil + case PrefixMatch: + return newPrefixNameRule(nextAction, auto, rewriteQuestionFrom, rewriteQuestionTo, answers), nil + case SuffixMatch: + return newSuffixNameRule(nextAction, auto, rewriteQuestionFrom, rewriteQuestionTo, answers), nil + case SubstringMatch: + return newSubstringNameRule(nextAction, auto, rewriteQuestionFrom, rewriteQuestionTo, answers), nil + case RegexMatch: + rewriteQuestionFromPattern, err := isValidRegexPattern(rewriteQuestionFrom, rewriteQuestionTo) + if err != nil { + return nil, err } + rewriteQuestionTo := plugin.Name(args[2]).Normalize() + return newRegexNameRule(nextAction, auto, rewriteQuestionFromPattern, rewriteQuestionTo, answers), nil + default: + return nil, fmt.Errorf("name rule supports only exact, prefix, suffix, substring, and regex name matching, received: %s", matchType) + } +} + +func parseAnswerRules(name string, args []string) (auto bool, rules ResponseRules, err error) { + auto = false + arg := 0 + nameRules := 0 + last := "" + if len(args) < 2 { + return false, nil, fmt.Errorf("invalid arguments for %s rule", name) } - //if len(args) == 7 { - if (len(args)-3)%4 == 0 { - if matchType == RegexMatch { - rewriteQuestionFromPattern, err := isValidRegexPattern(rewriteQuestionFrom, rewriteQuestionTo) + for arg < len(args) { + if last == "" && args[arg] != AnswerMatch { + if last == "" { + return false, nil, fmt.Errorf("exceeded the number of arguments for a non-answer rule argument for %s rule", name) + + } + return false, nil, fmt.Errorf("exceeded the number of arguments for %s answer rule for %s rule", last, name) + } + if args[arg] == AnswerMatch { + arg++ + } + if len(args)-arg == 0 { + return false, nil, fmt.Errorf("type missing for answer rule for %s rule", name) + } + last = args[arg] + arg++ + switch last { + case AutoMatch: + auto = true + continue + case NameMatch: + if len(args)-arg < 2 { + return false, nil, fmt.Errorf("%s answer rule for %s rule: 2 arguments required", last, name) + } + rewriteAnswerFrom := args[arg] + rewriteAnswerTo := args[arg+1] + rewriteAnswerFromPattern, err := isValidRegexPattern(rewriteAnswerFrom, rewriteAnswerTo) + rewriteAnswerTo = plugin.Name(rewriteAnswerTo).Normalize() if err != nil { - return nil, err + return false, nil, fmt.Errorf("%s answer rule for %s rule: %s", last, name, err) } - rewriteQuestionTo = plugin.Name(args[2]).Normalize() - - responseRuleCount := (len(args) - 3) / 4 - responseRules := make([]ResponseRule, responseRuleCount) - for i := 0; i < responseRuleCount; i++ { - startIdx := 3 + (i * 4) - responseRule, err := newResponseRule(args[startIdx : startIdx+4]) - if err != nil { - return nil, err - } - responseRules[i] = *responseRule + rules = append(rules, &nameRewriterResponseRule{newStringRewriter(rewriteAnswerFromPattern, rewriteAnswerTo)}) + arg += 2 + nameRules++ + case ValueMatch: + if len(args)-arg < 2 { + return false, nil, fmt.Errorf("%s answer rule for %s rule: 2 arguments required", last, name) } - - return ®exNameRule{ - nextAction, - rewriteQuestionFromPattern, - rewriteQuestionTo, - responseRules, - }, nil + rewriteAnswerFrom := args[arg] + rewriteAnswerTo := args[arg+1] + rewriteAnswerFromPattern, err := isValidRegexPattern(rewriteAnswerFrom, rewriteAnswerTo) + rewriteAnswerTo = plugin.Name(rewriteAnswerTo).Normalize() + if err != nil { + return false, nil, fmt.Errorf("%s answer rule for %s rule: %s", last, name, err) + } + rules = append(rules, &valueRewriterResponseRule{newStringRewriter(rewriteAnswerFromPattern, rewriteAnswerTo)}) + arg += 2 + default: + return false, nil, fmt.Errorf("invalid type %q for answer rule for %s rule", last, name) } - return nil, fmt.Errorf("the rewrite of response is supported only for name regex rule") } - return nil, fmt.Errorf("the rewrite rule is invalid: %s", args) -} -// newResponseRule creates a new "answer name" or "answer value" response rule. -func newResponseRule(args []string) (responseRule *ResponseRule, err error) { - if args[0] != "answer" { - return nil, fmt.Errorf("exceeded the number of arguments for a regex name rule") - } - rewriteAnswerField := strings.ToLower(args[1]) - switch rewriteAnswerField { - case "name": - case "value": - default: - return nil, fmt.Errorf("exceeded the number of arguments for a regex name rule") + if auto && nameRules > 0 { + return false, nil, fmt.Errorf("auto name answer rule cannot be combined with explicit name anwer rules") } - rewriteAnswerFrom := args[2] - rewriteAnswerTo := args[3] - rewriteAnswerFromPattern, err := isValidRegexPattern(rewriteAnswerFrom, rewriteAnswerTo) - if err != nil { - return nil, err - } - rewriteAnswerTo = plugin.Name(args[3]).Normalize() - - return &ResponseRule{ - Active: true, - Type: rewriteAnswerField, - Pattern: rewriteAnswerFromPattern, - Replacement: rewriteAnswerTo, - }, nil -} - -// Mode returns the processing nextAction -func (rule *exactNameRule) Mode() string { return rule.NextAction } -func (rule *prefixNameRule) Mode() string { return rule.NextAction } -func (rule *suffixNameRule) Mode() string { return rule.NextAction } -func (rule *substringNameRule) Mode() string { return rule.NextAction } -func (rule *regexNameRule) Mode() string { return rule.NextAction } - -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *exactNameRule) GetResponseRules() []ResponseRule { - return []ResponseRule{rule.ResponseRule} + return } -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *prefixNameRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } - -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *suffixNameRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } - -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *substringNameRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } - -// GetResponseRules returns rules to rewrite the response with. -func (rule *regexNameRule) GetResponseRules() []ResponseRule { return rule.ResponseRules } - // hasClosingDot returns true if s has a closing dot at the end. func hasClosingDot(s string) bool { return strings.HasSuffix(s, ".") diff --git a/plugin/rewrite/name_test.go b/plugin/rewrite/name_test.go index 0d3c3dd28..bd0112e39 100644 --- a/plugin/rewrite/name_test.go +++ b/plugin/rewrite/name_test.go @@ -16,9 +16,9 @@ func TestRewriteIllegalName(t *testing.T) { r, _ := newNameRule("stop", "example.org.", "example..org.") rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - Rules: []Rule{r}, - noRevert: true, + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRevertPolicy(), } ctx := context.TODO() @@ -55,9 +55,9 @@ func TestRewriteNamePrefixSuffix(t *testing.T) { } rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - Rules: []Rule{r}, - noRevert: true, + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRevertPolicy(), } m := new(dns.Msg) @@ -75,6 +75,237 @@ func TestRewriteNamePrefixSuffix(t *testing.T) { } } +func TestRewriteNameNoRewrite(t *testing.T) { + + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + expected string + }{ + {"stop", []string{"prefix", "foo", "bar"}, "coredns.foo.", "coredns.foo."}, + {"stop", []string{"prefix", "foo", "bar."}, "coredns.foo.", "coredns.foo."}, + {"stop", []string{"suffix", "com", "org"}, "com.coredns.", "com.coredns."}, + {"stop", []string{"suffix", "com", "org."}, "com.coredns.", "com.coredns."}, + {"stop", []string{"substring", "service", "svc"}, "com.coredns.", "com.coredns."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestRewriteNamePrefixSuffixNoAutoAnswer(t *testing.T) { + + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + expected string + }{ + {"stop", []string{"prefix", "foo", "bar"}, "foo.example.com.", "bar.example.com."}, + {"stop", []string{"prefix", "foo.", "bar."}, "foo.example.com.", "bar.example.com."}, + {"stop", []string{"suffix", "com", "org"}, "foo.example.com.", "foo.example.org."}, + {"stop", []string{"suffix", ".com", ".org"}, "foo.example.com.", "foo.example.org."}, + {"stop", []string{"suffix", ".ingress.coredns.rocks", "nginx.coredns.rocks"}, "coredns.ingress.coredns.rocks.", "corednsnginx.coredns.rocks."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestRewriteNamePrefixSuffixAutoAnswer(t *testing.T) { + + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + rewrite string + expected string + }{ + {"stop", []string{"prefix", "foo", "bar", "answer", "auto"}, "foo.example.com.", "bar.example.com.", "foo.example.com."}, + {"stop", []string{"prefix", "foo.", "bar.", "answer", "auto"}, "foo.example.com.", "bar.example.com.", "foo.example.com."}, + {"stop", []string{"suffix", "com", "org", "answer", "auto"}, "foo.example.com.", "foo.example.org.", "foo.example.com."}, + {"stop", []string{"suffix", ".com", ".org", "answer", "auto"}, "foo.example.com.", "foo.example.org.", "foo.example.com."}, + {"stop", []string{"suffix", ".ingress.coredns.rocks", "nginx.coredns.rocks", "answer", "auto"}, "coredns.ingress.coredns.rocks.", "corednsnginx.coredns.rocks.", "coredns.ingress.coredns.rocks."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRestorePolicy(), + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + rewrite := rec.Msg.Question[0].Name + if rewrite != tc.rewrite { + t.Fatalf("Test %d: Expected question rewrite to %v, got %v", i, tc.rewrite, rewrite) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestRewriteNameExactAnswer(t *testing.T) { + + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + rewrite string + expected string + }{ + {"stop", []string{"exact", "coredns.rocks", "service.consul", "answer", "auto"}, "coredns.rocks.", "service.consul.", "coredns.rocks."}, + {"stop", []string{"exact", "coredns.rocks.", "service.consul.", "answer", "auto"}, "coredns.rocks.", "service.consul.", "coredns.rocks."}, + {"stop", []string{"exact", "coredns.rocks", "service.consul"}, "coredns.rocks.", "service.consul.", "coredns.rocks."}, + {"stop", []string{"exact", "coredns.rocks.", "service.consul."}, "coredns.rocks.", "service.consul.", "coredns.rocks."}, + {"stop", []string{"exact", "coredns.org.", "service.consul."}, "coredns.rocks.", "coredns.rocks.", "coredns.rocks."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRestorePolicy(), + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + rewrite := rec.Msg.Question[0].Name + if rewrite != tc.rewrite { + t.Fatalf("Test %d: Expected question rewrite to %v, got %v", i, tc.rewrite, rewrite) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestRewriteNameRegexAnswer(t *testing.T) { + + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + rewrite string + expected string + }{ + {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.coredns.maps", "answer", "auto"}, "foo.coredns.rocks.", "foo.coredns.maps.", "foo.coredns.rocks."}, + {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.coredns.maps", "answer", "name", "(.*).coredns.maps", "{1}.coredns.works"}, "foo.coredns.rocks.", "foo.coredns.maps.", "foo.coredns.works."}, + {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.coredns.maps"}, "foo.coredns.rocks.", "foo.coredns.maps.", "foo.coredns.maps."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRestorePolicy(), + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + rewrite := rec.Msg.Question[0].Name + if rewrite != tc.rewrite { + t.Fatalf("Test %d: Expected question rewrite to %v, got %v", i, tc.rewrite, rewrite) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + func TestNewNameRule(t *testing.T) { tests := []struct { next string @@ -91,6 +322,26 @@ func TestNewNameRule(t *testing.T) { {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.coredns.rocks"}, false}, {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.{2}.coredns.rocks"}, true}, {"stop", []string{"regex", "staging.mydomain.com", "aws-loadbalancer-id.us-east-1.elb.amazonaws.com"}, false}, + {"stop", []string{"suffix", "staging.mydomain.com", "coredns.rock", "answer"}, true}, + {"stop", []string{"suffix", "staging.mydomain.com", "coredns.rock", "answer", "name"}, true}, + {"stop", []string{"suffix", "staging.mydomain.com", "coredns.rock", "answer", "other"}, true}, + {"stop", []string{"suffix", "staging.mydomain.com", "coredns.rock", "answer", "auto"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "auto"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name"}, true}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "coredns.rock", "staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.{2}.staging.mydomain.com"}, true}, + + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "name", "(.*).coredns.rock"}, true}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "value", "(.*).coredns.rock"}, true}, + + {"stop", []string{"suffix", "staging.mydomain.com.", "coredns.rock.", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"suffix", "staging.mydomain.com.", "coredns.rock.", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"suffix", "staging.mydomain.com.", "coredns.rock.", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"suffix", "staging.mydomain.com.", "coredns.rock.", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com", "value", "(.*).coredns.rock"}, true}, } for i, tc := range tests { failed := false diff --git a/plugin/rewrite/reverter.go b/plugin/rewrite/reverter.go index 49222ddfc..7d83e557b 100644 --- a/plugin/rewrite/reverter.go +++ b/plugin/rewrite/reverter.go @@ -1,37 +1,69 @@ package rewrite import ( - "regexp" - "strconv" - "strings" - "github.com/miekg/dns" ) +// RevertPolicy controls the overall reverting process +type RevertPolicy interface { + DoRevert() bool + DoQuestionRestore() bool +} + +type revertPolicy struct { + noRevert bool + noRestore bool +} + +func (p revertPolicy) DoRevert() bool { + return !p.noRevert +} + +func (p revertPolicy) DoQuestionRestore() bool { + return !p.noRestore +} + +// NoRevertPolicy disables all response rewrite rules +func NoRevertPolicy() RevertPolicy { + return revertPolicy{true, false} +} + +// NoRestorePolicy disables the question restoration during the response rewrite +func NoRestorePolicy() RevertPolicy { + return revertPolicy{false, true} +} + +// NewRevertPolicy creates a new reverter policy by dynamically specifying all +// options. +func NewRevertPolicy(noRevert, noRestore bool) RevertPolicy { + return revertPolicy{noRestore: noRestore, noRevert: noRevert} +} + // ResponseRule contains a rule to rewrite a response with. -type ResponseRule struct { - Active bool - Type string - Pattern *regexp.Regexp - Replacement string - TTL uint32 +type ResponseRule interface { + RewriteResponse(rr dns.RR) } +// ResponseRules describes an ordered list of response rules to apply +// after a name rewrite +type ResponseRules = []ResponseRule + // 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. // dig will complain with ';; Question section mismatch: got example.org/HINFO/IN' type ResponseReverter struct { dns.ResponseWriter originalQuestion dns.Question - ResponseRewrite bool - ResponseRules []ResponseRule + ResponseRules ResponseRules + revertPolicy RevertPolicy } // 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, policy RevertPolicy) *ResponseReverter { return &ResponseReverter{ ResponseWriter: w, originalQuestion: r.Question[0], + revertPolicy: policy, } } @@ -40,61 +72,33 @@ func (r *ResponseReverter) WriteMsg(res1 *dns.Msg) error { // Deep copy 'res' as to not (e.g). rewrite a message that's also stored in the cache. res := res1.Copy() - res.Question[0] = r.originalQuestion - if r.ResponseRewrite { + if r.revertPolicy.DoQuestionRestore() { + res.Question[0] = r.originalQuestion + } + if len(r.ResponseRules) > 0 { for _, rr := range res.Ns { - rewriteResourceRecord(res, rr, r) + r.rewriteResourceRecord(res, rr) } - for _, rr := range res.Answer { - rewriteResourceRecord(res, rr, r) + r.rewriteResourceRecord(res, rr) } - for _, rr := range res.Extra { - rewriteResourceRecord(res, rr, r) + r.rewriteResourceRecord(res, rr) } - } return r.ResponseWriter.WriteMsg(res) } -func rewriteResourceRecord(res *dns.Msg, rr dns.RR, r *ResponseReverter) { - var ( - isNameRewritten bool - isTTLRewritten bool - isValueRewritten bool - name = rr.Header().Name - ttl = rr.Header().Ttl - value string - ) - +func (r *ResponseReverter) rewriteResourceRecord(res *dns.Msg, rr dns.RR) { for _, rule := range r.ResponseRules { - if rule.Type == "" { - rule.Type = "name" - } - switch rule.Type { - case "name": - rewriteString(rule, &name, &isNameRewritten) - case "value": - value = getRecordValueForRewrite(rr) - if value != "" { - rewriteString(rule, &value, &isValueRewritten) - } - case "ttl": - ttl = rule.TTL - isTTLRewritten = true - } + rule.RewriteResponse(rr) } +} - if isNameRewritten { - rr.Header().Name = name - } - if isTTLRewritten { - rr.Header().Ttl = ttl - } - if isValueRewritten { - setRewrittenRecordValue(rr, value) - } +// Write is a wrapper that records the size of the message that gets written. +func (r *ResponseReverter) Write(buf []byte) (int, error) { + n, err := r.ResponseWriter.Write(buf) + return n, err } func getRecordValueForRewrite(rr dns.RR) (name string) { @@ -136,24 +140,3 @@ func setRewrittenRecordValue(rr dns.RR, value string) { rr.(*dns.SOA).Ns = value } } - -func rewriteString(rule ResponseRule, str *string, isStringRewritten *bool) { - regexGroups := rule.Pattern.FindStringSubmatch(*str) - if len(regexGroups) == 0 { - return - } - s := rule.Replacement - for groupIndex, groupValue := range regexGroups { - groupIndexStr := "{" + strconv.Itoa(groupIndex) + "}" - s = strings.Replace(s, groupIndexStr, groupValue, -1) - } - - *isStringRewritten = true - *str = s -} - -// Write is a wrapper that records the size of the message that gets written. -func (r *ResponseReverter) Write(buf []byte) (int, error) { - n, err := r.ResponseWriter.Write(buf) - return n, err -} diff --git a/plugin/rewrite/reverter_test.go b/plugin/rewrite/reverter_test.go index 7e31a707f..2c90d9bb6 100644 --- a/plugin/rewrite/reverter_test.go +++ b/plugin/rewrite/reverter_test.go @@ -53,9 +53,9 @@ func doReverterTests(rules []Rule, t *testing.T) { m.Question[0].Qclass = dns.ClassINET m.Answer = tc.answer rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - Rules: rules, - noRevert: tc.noRevert, + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + RevertPolicy: NewRevertPolicy(tc.noRevert, false), } rec := dnstest.NewRecorder(&test.ResponseWriter{}) rw.ServeDNS(ctx, rec, m) @@ -81,38 +81,56 @@ var valueTests = []struct { expectAnswerType uint16 expectAddlName string }{ - {"my.domain.uk", dns.TypeSRV, []dns.RR{test.SRV("my.cluster.local. 5 IN SRV 0 100 100 srv1.my.cluster.local.")}, []dns.RR{test.A("srv1.my.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk", dns.TypeSRV, false, "srv1.my.domain.uk.", dns.TypeSRV, "srv1.my.domain.uk."}, - {"my.domain.uk", dns.TypeSRV, []dns.RR{test.SRV("my.cluster.local. 5 IN SRV 0 100 100 srv1.my.cluster.local.")}, []dns.RR{test.A("srv1.my.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeSRV, true, "srv1.my.cluster.local.", dns.TypeSRV, "srv1.my.cluster.local."}, - {"my.domain.uk", dns.TypeANY, []dns.RR{test.CNAME("my.cluster.local. 3600 IN CNAME cname.cluster.local.")}, []dns.RR{test.A("cname.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk", dns.TypeANY, false, "cname.domain.uk.", dns.TypeCNAME, "cname.domain.uk."}, - {"my.domain.uk", dns.TypeANY, []dns.RR{test.CNAME("my.cluster.local. 3600 IN CNAME cname.cluster.local.")}, []dns.RR{test.A("cname.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "cname.cluster.local.", dns.TypeCNAME, "cname.cluster.local."}, - {"my.domain.uk", dns.TypeANY, []dns.RR{test.DNAME("my.cluster.local. 3600 IN DNAME dname.cluster.local.")}, []dns.RR{test.A("dname.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk", dns.TypeANY, false, "dname.domain.uk.", dns.TypeDNAME, "dname.domain.uk."}, - {"my.domain.uk", dns.TypeANY, []dns.RR{test.DNAME("my.cluster.local. 3600 IN DNAME dname.cluster.local.")}, []dns.RR{test.A("dname.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "dname.cluster.local.", dns.TypeDNAME, "dname.cluster.local."}, - {"my.domain.uk", dns.TypeMX, []dns.RR{test.MX("my.cluster.local. 3600 IN MX 1 mx1.cluster.local.")}, []dns.RR{test.A("mx1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk", dns.TypeMX, false, "mx1.domain.uk.", dns.TypeMX, "mx1.domain.uk."}, - {"my.domain.uk", dns.TypeMX, []dns.RR{test.MX("my.cluster.local. 3600 IN MX 1 mx1.cluster.local.")}, []dns.RR{test.A("mx1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeMX, true, "mx1.cluster.local.", dns.TypeMX, "mx1.cluster.local."}, - {"my.domain.uk", dns.TypeANY, []dns.RR{test.NS("my.cluster.local. 3600 IN NS ns1.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk", dns.TypeANY, false, "ns1.domain.uk.", dns.TypeNS, "ns1.domain.uk."}, - {"my.domain.uk", dns.TypeANY, []dns.RR{test.NS("my.cluster.local. 3600 IN NS ns1.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "ns1.cluster.local.", dns.TypeNS, "ns1.cluster.local."}, - {"my.domain.uk", dns.TypeSOA, []dns.RR{test.SOA("my.cluster.local. 1800 IN SOA ns1.cluster.local. admin.cluster.local. 1502165581 14400 3600 604800 14400")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk", dns.TypeSOA, false, "ns1.domain.uk.", dns.TypeSOA, "ns1.domain.uk."}, - {"my.domain.uk", dns.TypeSOA, []dns.RR{test.SOA("my.cluster.local. 1800 IN SOA ns1.cluster.local. admin.cluster.local. 1502165581 14400 3600 604800 14400")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeSOA, true, "ns1.cluster.local.", dns.TypeSOA, "ns1.cluster.local."}, - {"my.domain.uk", dns.TypeNAPTR, []dns.RR{test.NAPTR("my.cluster.local. 100 IN NAPTR 100 10 \"S\" \"SIP+D2U\" \"!^.*$!sip:customer-service@example.com!\" _sip._udp.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk", dns.TypeNAPTR, false, "_sip._udp.domain.uk.", dns.TypeNAPTR, "ns1.domain.uk."}, - {"my.domain.uk", dns.TypeNAPTR, []dns.RR{test.NAPTR("my.cluster.local. 100 IN NAPTR 100 10 \"S\" \"SIP+D2U\" \"!^.*$!sip:customer-service@example.com!\" _sip._udp.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeNAPTR, true, "_sip._udp.cluster.local.", dns.TypeNAPTR, "ns1.cluster.local."}, + {"my.domain.uk.", dns.TypeSRV, []dns.RR{test.SRV("my.cluster.local. 5 IN SRV 0 100 100 srv1.my.cluster.local.")}, []dns.RR{test.A("srv1.my.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeSRV, false, "srv1.my.domain.uk.", dns.TypeSRV, "srv1.my.domain.uk."}, + {"my.domain.uk.", dns.TypeSRV, []dns.RR{test.SRV("my.cluster.local. 5 IN SRV 0 100 100 srv1.my.cluster.local.")}, []dns.RR{test.A("srv1.my.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeSRV, true, "srv1.my.cluster.local.", dns.TypeSRV, "srv1.my.cluster.local."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.CNAME("my.cluster.local. 3600 IN CNAME cname.cluster.local.")}, []dns.RR{test.A("cname.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeANY, false, "cname.domain.uk.", dns.TypeCNAME, "cname.domain.uk."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.CNAME("my.cluster.local. 3600 IN CNAME cname.cluster.local.")}, []dns.RR{test.A("cname.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "cname.cluster.local.", dns.TypeCNAME, "cname.cluster.local."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.DNAME("my.cluster.local. 3600 IN DNAME dname.cluster.local.")}, []dns.RR{test.A("dname.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeANY, false, "dname.domain.uk.", dns.TypeDNAME, "dname.domain.uk."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.DNAME("my.cluster.local. 3600 IN DNAME dname.cluster.local.")}, []dns.RR{test.A("dname.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "dname.cluster.local.", dns.TypeDNAME, "dname.cluster.local."}, + {"my.domain.uk.", dns.TypeMX, []dns.RR{test.MX("my.cluster.local. 3600 IN MX 1 mx1.cluster.local.")}, []dns.RR{test.A("mx1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeMX, false, "mx1.domain.uk.", dns.TypeMX, "mx1.domain.uk."}, + {"my.domain.uk.", dns.TypeMX, []dns.RR{test.MX("my.cluster.local. 3600 IN MX 1 mx1.cluster.local.")}, []dns.RR{test.A("mx1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeMX, true, "mx1.cluster.local.", dns.TypeMX, "mx1.cluster.local."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.NS("my.cluster.local. 3600 IN NS ns1.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeANY, false, "ns1.domain.uk.", dns.TypeNS, "ns1.domain.uk."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.NS("my.cluster.local. 3600 IN NS ns1.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "ns1.cluster.local.", dns.TypeNS, "ns1.cluster.local."}, + {"my.domain.uk.", dns.TypeSOA, []dns.RR{test.SOA("my.cluster.local. 1800 IN SOA ns1.cluster.local. admin.cluster.local. 1502165581 14400 3600 604800 14400")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeSOA, false, "ns1.domain.uk.", dns.TypeSOA, "ns1.domain.uk."}, + {"my.domain.uk.", dns.TypeSOA, []dns.RR{test.SOA("my.cluster.local. 1800 IN SOA ns1.cluster.local. admin.cluster.local. 1502165581 14400 3600 604800 14400")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeSOA, true, "ns1.cluster.local.", dns.TypeSOA, "ns1.cluster.local."}, + {"my.domain.uk.", dns.TypeNAPTR, []dns.RR{test.NAPTR("my.cluster.local. 100 IN NAPTR 100 10 \"S\" \"SIP+D2U\" \"!^.*$!sip:customer-service@example.com!\" _sip._udp.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeNAPTR, false, "_sip._udp.domain.uk.", dns.TypeNAPTR, "ns1.domain.uk."}, + {"my.domain.uk.", dns.TypeNAPTR, []dns.RR{test.NAPTR("my.cluster.local. 100 IN NAPTR 100 10 \"S\" \"SIP+D2U\" \"!^.*$!sip:customer-service@example.com!\" _sip._udp.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeNAPTR, true, "_sip._udp.cluster.local.", dns.TypeNAPTR, "ns1.cluster.local."}, } func TestValueResponseReverter(t *testing.T) { rules := []Rule{} - r, _ := newNameRule("stop", "regex", `(.*)\.domain\.uk`, "{1}.cluster.local", "answer", "name", `(.*)\.cluster\.local`, "{1}.domain.uk", "answer", "value", `(.*)\.cluster\.local`, "{1}.domain.uk") + r, err := newNameRule("stop", "regex", `(.*)\.domain\.uk`, "{1}.cluster.local", "answer", "name", `(.*)\.cluster\.local`, "{1}.domain.uk", "answer", "value", `(.*)\.cluster\.local`, "{1}.domain.uk") + if err != nil { + t.Errorf("cannot parse rule: %s", err) + return + } rules = append(rules, r) - doValueReverterTests(rules, t) + doValueReverterTests("stop", rules, t) rules = []Rule{} - r, _ = newNameRule("continue", "regex", `(.*)\.domain\.uk`, "{1}.cluster.local", "answer", "name", `(.*)\.cluster\.local`, "{1}.domain.uk", "answer", "value", `(.*)\.cluster\.local`, "{1}.domain.uk") + r, err = newNameRule("continue", "regex", `(.*)\.domain\.uk`, "{1}.cluster.local", "answer", "name", `(.*)\.cluster\.local`, "{1}.domain.uk", "answer", "value", `(.*)\.cluster\.local`, "{1}.domain.uk") + if err != nil { + t.Errorf("cannot parse rule: %s", err) + return + } rules = append(rules, r) - doValueReverterTests(rules, t) + doValueReverterTests("continue", rules, t) + + rules = []Rule{} + r, err = newNameRule("stop", "suffix", `.domain.uk`, ".cluster.local", "answer", "auto", "answer", "value", `(.*)\.cluster\.local`, "{1}.domain.uk") + if err != nil { + t.Errorf("cannot parse rule: %s", err) + return + } + rules = append(rules, r) + + doValueReverterTests("suffix", rules, t) } -func doValueReverterTests(rules []Rule, t *testing.T) { +func doValueReverterTests(name string, rules []Rule, t *testing.T) { ctx := context.TODO() for i, tc := range valueTests { m := new(dns.Msg) @@ -121,37 +139,41 @@ func doValueReverterTests(rules []Rule, t *testing.T) { m.Answer = tc.answer m.Extra = tc.extra rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - Rules: rules, - noRevert: tc.noRevert, + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + RevertPolicy: NewRevertPolicy(tc.noRevert, false), } rec := dnstest.NewRecorder(&test.ResponseWriter{}) rw.ServeDNS(ctx, rec, m) resp := rec.Msg if resp.Question[0].Name != tc.to { - t.Errorf("Test %d: Expected Name to be %q but was %q", i, tc.to, resp.Question[0].Name) + t.Errorf("Test %s.%d: Expected Name to be %q but was %q", name, i, tc.to, resp.Question[0].Name) } if resp.Question[0].Qtype != tc.toType { - t.Errorf("Test %d: Expected Type to be '%d' but was '%d'", i, tc.toType, resp.Question[0].Qtype) + t.Errorf("Test %s.%d: Expected Type to be '%d' but was '%d'", name, i, tc.toType, resp.Question[0].Qtype) } - if len(resp.Answer) <= 0 || resp.Answer[0].Header().Rrtype != tc.expectAnswerType { - t.Error("Unexpected Answer Record Type / No Answers") + if len(resp.Answer) <= 0 { + t.Errorf("Test %s.%d: No Answers", name, i) + return + } + if len(resp.Answer) > 0 && resp.Answer[0].Header().Rrtype != tc.expectAnswerType { + t.Errorf("Test %s.%d: Unexpected Answer Record Type %d", name, i, resp.Answer[0].Header().Rrtype) return } value := getRecordValueForRewrite(resp.Answer[0]) if value != tc.expectValue { - t.Errorf("Test %d: Expected Target to be '%s' but was '%s'", i, tc.expectValue, value) + t.Errorf("Test %s.%d: Expected Target to be '%s' but was '%s'", name, i, tc.expectValue, value) } if len(resp.Extra) <= 0 || resp.Extra[0].Header().Rrtype != dns.TypeA { - t.Error("Unexpected Additional Record Type / No Additional Records") + t.Errorf("Test %s.%d: Unexpected Additional Record Type / No Additional Records", name, i) return } if resp.Extra[0].Header().Name != tc.expectAddlName { - t.Errorf("Test %d: Expected Extra Name to be %q but was %q", i, tc.expectAddlName, resp.Extra[0].Header().Name) + t.Errorf("Test %s.%d: Expected Extra Name to be %q but was %q", name, i, tc.expectAddlName, resp.Extra[0].Header().Name) } } } diff --git a/plugin/rewrite/rewrite.go b/plugin/rewrite/rewrite.go index f44cc62f1..188418ca2 100644 --- a/plugin/rewrite/rewrite.go +++ b/plugin/rewrite/rewrite.go @@ -31,40 +31,34 @@ const ( // Rewrite is a plugin to rewrite requests internally before being handled. type Rewrite struct { - Next plugin.Handler - Rules []Rule - noRevert bool + Next plugin.Handler + Rules []Rule + RevertPolicy } // ServeDNS implements the plugin.Handler interface. func (rw Rewrite) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { - wr := NewResponseReverter(w, r) + if rw.RevertPolicy == nil { + rw.RevertPolicy = NewRevertPolicy(false, false) + } + wr := NewResponseReverter(w, r, rw.RevertPolicy) state := request.Request{W: w, Req: r} for _, rule := range rw.Rules { - switch result := rule.Rewrite(ctx, state); result { - case RewriteDone: + respRules, result := rule.Rewrite(ctx, state) + if result == RewriteDone { if _, ok := dns.IsDomainName(state.Req.Question[0].Name); !ok { err := fmt.Errorf("invalid name after rewrite: %s", state.Req.Question[0].Name) state.Req.Question[0] = wr.originalQuestion return dns.RcodeServerFailure, err } - for _, respRule := range rule.GetResponseRules() { - if respRule.Active { - wr.ResponseRewrite = true - wr.ResponseRules = append(wr.ResponseRules, respRule) - } - } + wr.ResponseRules = append(wr.ResponseRules, respRules...) if rule.Mode() == Stop { - if rw.noRevert { - return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, w, r) - } - return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, wr, r) + break } - case RewriteIgnored: } } - if rw.noRevert || len(wr.ResponseRules) == 0 { + if !rw.RevertPolicy.DoRevert() || len(wr.ResponseRules) == 0 { return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, w, r) } return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, wr, r) @@ -76,11 +70,9 @@ func (rw Rewrite) Name() string { return "rewrite" } // Rule describes a rewrite rule. type Rule interface { // Rewrite rewrites the current request. - Rewrite(ctx context.Context, state request.Request) Result + Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) // Mode returns the processing mode stop or continue. Mode() string - // GetResponseRules returns rules to rewrite response with, if any. - GetResponseRules() []ResponseRule } func newRule(args ...string) (Rule, error) { diff --git a/plugin/rewrite/rewrite_test.go b/plugin/rewrite/rewrite_test.go index ecab07787..260b65c82 100644 --- a/plugin/rewrite/rewrite_test.go +++ b/plugin/rewrite/rewrite_test.go @@ -3,6 +3,7 @@ package rewrite import ( "bytes" "context" + "fmt" "reflect" "testing" @@ -16,6 +17,11 @@ import ( ) func msgPrinter(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if len(r.Answer) == 0 { + r.Answer = []dns.RR{ + test.A(fmt.Sprintf("%s 5 IN A 10.0.0.1", r.Question[0].Name)), + } + } w.WriteMsg(r) return 0, nil } @@ -44,7 +50,7 @@ func TestNewRule(t *testing.T) { {[]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", "fcore.dns.rocks", "dns.fcore.rocks", "answer", "name", "(fcore)\\.(dns)\\.(rocks)", "{2}.{1}.{3}"}, false, reflect.TypeOf(&substringNameRule{})}, {[]string{"name", "substring", "a.com", "b.com", "c.com"}, true, nil}, {[]string{"type"}, true, nil}, {[]string{"type", "a"}, true, nil}, @@ -185,9 +191,9 @@ func TestRewrite(t *testing.T) { rules = append(rules, r) rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - Rules: rules, - noRevert: true, + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + RevertPolicy: NoRevertPolicy(), } tests := []struct { @@ -248,8 +254,8 @@ func TestRewrite(t *testing.T) { func TestRewriteEDNS0Local(t *testing.T) { rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - noRevert: true, + Next: plugin.HandlerFunc(msgPrinter), + RevertPolicy: NoRevertPolicy(), } tests := []struct { @@ -336,9 +342,9 @@ func TestEdns0LocalMultiRule(t *testing.T) { rules = append(rules, r) rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - Rules: rules, - noRevert: true, + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + RevertPolicy: NoRevertPolicy(), } tests := []struct { @@ -447,8 +453,8 @@ func (tp testProvider) Metadata(ctx context.Context, state request.Request) cont func TestRewriteEDNS0LocalVariable(t *testing.T) { rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - noRevert: true, + Next: plugin.HandlerFunc(msgPrinter), + RevertPolicy: NoRevertPolicy(), } expectedMetadata := []metadata.Provider{ @@ -569,8 +575,8 @@ func TestRewriteEDNS0LocalVariable(t *testing.T) { func TestRewriteEDNS0Subnet(t *testing.T) { rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - noRevert: true, + Next: plugin.HandlerFunc(msgPrinter), + RevertPolicy: NoRevertPolicy(), } tests := []struct { diff --git a/plugin/rewrite/setup_test.go b/plugin/rewrite/setup_test.go index dcad03961..00808bd9c 100644 --- a/plugin/rewrite/setup_test.go +++ b/plugin/rewrite/setup_test.go @@ -44,12 +44,12 @@ func TestParse(t *testing.T) { `rewrite stop { name regex foo bar answer name bar foo - name baz qux + name baz }`) _, err = rewriteParse(c) if err == nil { t.Errorf("Expected error but got success for invalid response rewrite") - } else if !strings.Contains(err.Error(), "must consist only of") { + } else if !strings.Contains(err.Error(), "2 arguments required") { t.Errorf("Got wrong error for invalid response rewrite: %v", err.Error()) } diff --git a/plugin/rewrite/ttl.go b/plugin/rewrite/ttl.go index 6af9cdc40..364583dc7 100644 --- a/plugin/rewrite/ttl.go +++ b/plugin/rewrite/ttl.go @@ -9,81 +9,91 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/request" - //"github.com/miekg/dns" + + "github.com/miekg/dns" ) +type ttlResponseRule struct { + TTL uint32 +} + +func (r *ttlResponseRule) RewriteResponse(rr dns.RR) { + rr.Header().Ttl = r.TTL +} + +type ttlRuleBase struct { + nextAction string + response ttlResponseRule +} + +func newTTLRuleBase(nextAction string, ttl uint32) ttlRuleBase { + return ttlRuleBase{ + nextAction: nextAction, + response: ttlResponseRule{TTL: ttl}, + } +} + +func (rule *ttlRuleBase) responseRule(match bool) (ResponseRules, Result) { + if match { + return ResponseRules{&rule.response}, RewriteDone + } + return nil, RewriteIgnored +} + +// Mode returns the processing nextAction +func (rule *ttlRuleBase) Mode() string { return rule.nextAction } + type exactTTLRule struct { - NextAction string - From string - ResponseRules []ResponseRule + ttlRuleBase + From string } type prefixTTLRule struct { - NextAction string - Prefix string - ResponseRules []ResponseRule + ttlRuleBase + Prefix string } type suffixTTLRule struct { - NextAction string - Suffix string - ResponseRules []ResponseRule + ttlRuleBase + Suffix string } type substringTTLRule struct { - NextAction string - Substring string - ResponseRules []ResponseRule + ttlRuleBase + Substring string } type regexTTLRule struct { - NextAction string - Pattern *regexp.Regexp - ResponseRules []ResponseRule + ttlRuleBase + Pattern *regexp.Regexp } // Rewrite rewrites the current request based upon exact match of the name // in the question section of the request. -func (rule *exactTTLRule) Rewrite(ctx context.Context, state request.Request) Result { - if rule.From == state.Name() { - return RewriteDone - } - return RewriteIgnored +func (rule *exactTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(rule.From == state.Name()) } // Rewrite rewrites the current request when the name begins with the matching string. -func (rule *prefixTTLRule) Rewrite(ctx context.Context, state request.Request) Result { - if strings.HasPrefix(state.Name(), rule.Prefix) { - return RewriteDone - } - return RewriteIgnored +func (rule *prefixTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(strings.HasPrefix(state.Name(), rule.Prefix)) } // Rewrite rewrites the current request when the name ends with the matching string. -func (rule *suffixTTLRule) Rewrite(ctx context.Context, state request.Request) Result { - if strings.HasSuffix(state.Name(), rule.Suffix) { - return RewriteDone - } - return RewriteIgnored +func (rule *suffixTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(strings.HasSuffix(state.Name(), rule.Suffix)) } // Rewrite rewrites the current request based upon partial match of the // name in the question section of the request. -func (rule *substringTTLRule) Rewrite(ctx context.Context, state request.Request) Result { - if strings.Contains(state.Name(), rule.Substring) { - return RewriteDone - } - return RewriteIgnored +func (rule *substringTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(strings.Contains(state.Name(), rule.Substring)) } // Rewrite rewrites the current request when the name in the question // section of the request matches a regular expression. -func (rule *regexTTLRule) Rewrite(ctx context.Context, state request.Request) Result { - regexGroups := rule.Pattern.FindStringSubmatch(state.Name()) - if len(regexGroups) == 0 { - return RewriteIgnored - } - return RewriteDone +func (rule *regexTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(len(rule.Pattern.FindStringSubmatch(state.Name())) != 0) } // newTTLRule creates a name matching rule based on exact, partial, or regex match @@ -106,43 +116,23 @@ func newTTLRule(nextAction string, args ...string) (Rule, error) { switch strings.ToLower(args[0]) { case ExactMatch: return &exactTTLRule{ - nextAction, + newTTLRuleBase(nextAction, ttl), plugin.Name(args[1]).Normalize(), - []ResponseRule{{ - Active: true, - Type: "ttl", - TTL: ttl, - }}, }, nil case PrefixMatch: return &prefixTTLRule{ - nextAction, + newTTLRuleBase(nextAction, ttl), plugin.Name(args[1]).Normalize(), - []ResponseRule{{ - Active: true, - Type: "ttl", - TTL: ttl, - }}, }, nil case SuffixMatch: return &suffixTTLRule{ - nextAction, + newTTLRuleBase(nextAction, ttl), plugin.Name(args[1]).Normalize(), - []ResponseRule{{ - Active: true, - Type: "ttl", - TTL: ttl, - }}, }, nil case SubstringMatch: return &substringTTLRule{ - nextAction, + newTTLRuleBase(nextAction, ttl), plugin.Name(args[1]).Normalize(), - []ResponseRule{{ - Active: true, - Type: "ttl", - TTL: ttl, - }}, }, nil case RegexMatch: regexPattern, err := regexp.Compile(args[1]) @@ -150,13 +140,8 @@ func newTTLRule(nextAction string, args ...string) (Rule, error) { return nil, fmt.Errorf("invalid regex pattern in a ttl rule: %s", args[1]) } return ®exTTLRule{ - nextAction, + newTTLRuleBase(nextAction, ttl), regexPattern, - []ResponseRule{{ - Active: true, - Type: "ttl", - TTL: ttl, - }}, }, nil default: return nil, fmt.Errorf("ttl rule supports only exact, prefix, suffix, substring, and regex name matching") @@ -166,48 +151,11 @@ func newTTLRule(nextAction string, args ...string) (Rule, error) { return nil, fmt.Errorf("many few arguments for a ttl rule") } return &exactTTLRule{ - nextAction, + newTTLRuleBase(nextAction, ttl), plugin.Name(args[0]).Normalize(), - []ResponseRule{{ - Active: true, - Type: "ttl", - TTL: ttl, - }}, }, nil } -// Mode returns the processing nextAction -func (rule *exactTTLRule) Mode() string { return rule.NextAction } -func (rule *prefixTTLRule) Mode() string { return rule.NextAction } -func (rule *suffixTTLRule) Mode() string { return rule.NextAction } -func (rule *substringTTLRule) Mode() string { return rule.NextAction } -func (rule *regexTTLRule) Mode() string { return rule.NextAction } - -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *exactTTLRule) GetResponseRules() []ResponseRule { - return rule.ResponseRules -} - -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *prefixTTLRule) GetResponseRules() []ResponseRule { - return rule.ResponseRules -} - -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *suffixTTLRule) GetResponseRules() []ResponseRule { - return rule.ResponseRules -} - -// GetResponseRules returns rules to rewrite the response with. Currently not implemented. -func (rule *substringTTLRule) GetResponseRules() []ResponseRule { - return rule.ResponseRules -} - -// GetResponseRules returns rules to rewrite the response with. -func (rule *regexTTLRule) GetResponseRules() []ResponseRule { - return rule.ResponseRules -} - // validTTL returns true if v is valid TTL value. func isValidTTL(v string) (uint32, bool) { i, err := strconv.Atoi(v) diff --git a/plugin/rewrite/ttl_test.go b/plugin/rewrite/ttl_test.go index 359807299..9ac0807ac 100644 --- a/plugin/rewrite/ttl_test.go +++ b/plugin/rewrite/ttl_test.go @@ -121,9 +121,8 @@ func doTTLTests(rules []Rule, t *testing.T) { m.Question[0].Qclass = dns.ClassINET m.Answer = tc.answer rw := Rewrite{ - Next: plugin.HandlerFunc(msgPrinter), - Rules: rules, - noRevert: false, + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, } rec := dnstest.NewRecorder(&test.ResponseWriter{}) rw.ServeDNS(ctx, rec, m) diff --git a/plugin/rewrite/type.go b/plugin/rewrite/type.go index 44f5047b5..63796e9dc 100644 --- a/plugin/rewrite/type.go +++ b/plugin/rewrite/type.go @@ -31,18 +31,15 @@ func newTypeRule(nextAction string, args ...string) (Rule, error) { } // Rewrite rewrites the current request. -func (rule *typeRule) Rewrite(ctx context.Context, state request.Request) Result { +func (rule *typeRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { if rule.fromType > 0 && rule.toType > 0 { if state.QType() == rule.fromType { state.Req.Question[0].Qtype = rule.toType - return RewriteDone + return nil, RewriteDone } } - return RewriteIgnored + return nil, RewriteIgnored } // Mode returns the processing mode. func (rule *typeRule) Mode() string { return rule.nextAction } - -// GetResponseRules return rules to rewrite the response with. Currently not implemented. -func (rule *typeRule) GetResponseRules() []ResponseRule { return []ResponseRule{} } |