diff options
author | 2023-04-13 17:49:36 +0530 | |
---|---|---|
committer | 2023-04-13 08:19:36 -0400 | |
commit | 8e8231d627a690415da3df7b464cf45a00c0defa (patch) | |
tree | 8dd16dea8f363ed59849f1fff1f826f45ede8476 /plugin | |
parent | 0063d7a80c0db18069429c775e4b95a5c0b4b69c (diff) | |
download | coredns-8e8231d627a690415da3df7b464cf45a00c0defa.tar.gz coredns-8e8231d627a690415da3df7b464cf45a00c0defa.tar.zst coredns-8e8231d627a690415da3df7b464cf45a00c0defa.zip |
[rewrite] Introduce cname target rewrite rule to rewrite plugin (#6004)
* cname target rewrite part in answer sec
tion
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* upstream request
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* fix looping issue
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* support exact, prefix, suffix, substring, and regex types for cname rewrite
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* support any qtype, corrected prefix, suffix, substring types behavior
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* unit tests added, mocked the upstream call
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* fix lint errors
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* add newline to fix test issue
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* add default rewrite type, add readme
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* readme grammar fix
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* reuse rewrite types
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
* comment fixed
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
---------
Signed-off-by: amila <amila.15@cse.mrt.ac.lk>
Diffstat (limited to 'plugin')
-rw-r--r-- | plugin/rewrite/README.md | 47 | ||||
-rw-r--r-- | plugin/rewrite/cname_target.go | 145 | ||||
-rw-r--r-- | plugin/rewrite/cname_target_test.go | 163 | ||||
-rw-r--r-- | plugin/rewrite/name.go | 4 | ||||
-rw-r--r-- | plugin/rewrite/reverter.go | 4 | ||||
-rw-r--r-- | plugin/rewrite/rewrite.go | 2 | ||||
-rw-r--r-- | plugin/rewrite/ttl.go | 2 |
7 files changed, 362 insertions, 5 deletions
diff --git a/plugin/rewrite/README.md b/plugin/rewrite/README.md index b460989ce..ef938e79d 100644 --- a/plugin/rewrite/README.md +++ b/plugin/rewrite/README.md @@ -25,6 +25,7 @@ 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`. * `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. + * `cname` - the CNAME target if the response has a CNAME record * **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 @@ -404,3 +405,49 @@ 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. + + +### CNAME Feild Rewrites + +There might be a scenario where you want the `CNAME` target of the response to be rewritten. You can do this by using the `CNAME` field rewrite. This will generate new answer records according to the new `CNAME` target. + +The syntax for the CNAME 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] cname [exact|prefix|suffix|substring|regex] FROM TO +``` + +Consider the following `CNAME` rewrite rule with regex type. +``` +rewrite cname regex (.*).cdn.example.net. {1}.other.cdn.com. +``` + +If you were to send the following DNS request without the above rule, an example response would be: + +``` +$ dig @10.1.1.1 my-app.com + +;; QUESTION SECTION: +;my-app.com. IN A + +;; ANSWER SECTION: +my-app.com. 200 IN CNAME my-app.com.cdn.example.net. +my-app.com.cdn.example.net. 300 IN A 20.2.0.1 +my-app.com.cdn.example.net. 300 IN A 20.2.0.2 +``` + +If you were to send the same DNS request with the above rule set up, an example response would be: + +``` +$ dig @10.1.1.1 my-app.com + +;; QUESTION SECTION: +;my-app.com. IN A + +;; ANSWER SECTION: +my-app.com. 200 IN CNAME my-app.com.other.cdn.com. +my-app.com.other.cdn.com. 100 IN A 30.3.1.2 +``` +Note that the answer will contain a completely different set of answer records after rewriting the `CNAME` target. diff --git a/plugin/rewrite/cname_target.go b/plugin/rewrite/cname_target.go new file mode 100644 index 000000000..c7d93e8a7 --- /dev/null +++ b/plugin/rewrite/cname_target.go @@ -0,0 +1,145 @@ +package rewrite + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// UpstreamInt wraps the Upstream API for dependency injection during testing +type UpstreamInt interface { + Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) +} + +// cnameTargetRule is cname target rewrite rule. +type cnameTargetRule struct { + rewriteType string + paramFromTarget string + paramToTarget string + nextAction string + state request.Request + ctx context.Context + Upstream UpstreamInt // Upstream for looking up external names during the resolution process. +} + +func (r *cnameTargetRule) getFromAndToTarget(inputCName string) (from string, to string) { + switch r.rewriteType { + case ExactMatch: + return r.paramFromTarget, r.paramToTarget + case PrefixMatch: + if strings.HasPrefix(inputCName, r.paramFromTarget) { + return inputCName, r.paramToTarget + strings.TrimPrefix(inputCName, r.paramFromTarget) + } + case SuffixMatch: + if strings.HasSuffix(inputCName, r.paramFromTarget) { + return inputCName, strings.TrimSuffix(inputCName, r.paramFromTarget) + r.paramToTarget + } + case SubstringMatch: + if strings.Contains(inputCName, r.paramFromTarget) { + return inputCName, strings.Replace(inputCName, r.paramFromTarget, r.paramToTarget, -1) + } + case RegexMatch: + pattern := regexp.MustCompile(r.paramFromTarget) + regexGroups := pattern.FindStringSubmatch(inputCName) + if len(regexGroups) == 0 { + return "", "" + } + substitution := r.paramToTarget + for groupIndex, groupValue := range regexGroups { + groupIndexStr := "{" + strconv.Itoa(groupIndex) + "}" + substitution = strings.Replace(substitution, groupIndexStr, groupValue, -1) + } + return inputCName, substitution + } + return "", "" +} + +func (r *cnameTargetRule) RewriteResponse(res *dns.Msg, rr dns.RR) { + // logic to rewrite the cname target of dns response + switch rr.Header().Rrtype { + case dns.TypeCNAME: + // rename the target of the cname response + if cname, ok := rr.(*dns.CNAME); ok { + fromTarget, toTarget := r.getFromAndToTarget(cname.Target) + if cname.Target == fromTarget { + // create upstream request with the new target with the same qtype + r.state.Req.Question[0].Name = toTarget + upRes, err := r.Upstream.Lookup(r.ctx, r.state, toTarget, r.state.Req.Question[0].Qtype) + + if err != nil { + log.Errorf("Error upstream request %v", err) + } + + var newAnswer []dns.RR + // iterate over first upstram response + // add the cname record to the new answer + for _, rr := range res.Answer { + if cname, ok := rr.(*dns.CNAME); ok { + // change the target name in the response + cname.Target = toTarget + newAnswer = append(newAnswer, rr) + } + } + // iterate over upstream response recieved + for _, rr := range upRes.Answer { + if rr.Header().Name == toTarget { + newAnswer = append(newAnswer, rr) + } + } + res.Answer = newAnswer + } + } + } +} + +func newCNAMERule(nextAction string, args ...string) (Rule, error) { + var rewriteType string + var paramFromTarget, paramToTarget string + if len(args) == 3 { + rewriteType = (strings.ToLower(args[0])) + switch rewriteType { + case ExactMatch: + case PrefixMatch: + case SuffixMatch: + case SubstringMatch: + case RegexMatch: + default: + return nil, fmt.Errorf("unknown cname rewrite type: %s", rewriteType) + } + paramFromTarget, paramToTarget = strings.ToLower(args[1]), strings.ToLower(args[2]) + } else if len(args) == 2 { + rewriteType = ExactMatch + paramFromTarget, paramToTarget = strings.ToLower(args[0]), strings.ToLower(args[1]) + } else { + return nil, fmt.Errorf("too few (%d) arguments for a cname rule", len(args)) + } + rule := cnameTargetRule{ + rewriteType: rewriteType, + paramFromTarget: paramFromTarget, + paramToTarget: paramToTarget, + nextAction: nextAction, + Upstream: upstream.New(), + } + return &rule, nil +} + +// Rewrite rewrites the current request. +func (r *cnameTargetRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if len(r.rewriteType) > 0 && len(r.paramFromTarget) > 0 && len(r.paramToTarget) > 0 { + r.state = state + r.ctx = ctx + return ResponseRules{r}, RewriteDone + } + return nil, RewriteIgnored +} + +// Mode returns the processing mode. +func (r *cnameTargetRule) Mode() string { return r.nextAction } diff --git a/plugin/rewrite/cname_target_test.go b/plugin/rewrite/cname_target_test.go new file mode 100644 index 000000000..04ca01af4 --- /dev/null +++ b/plugin/rewrite/cname_target_test.go @@ -0,0 +1,163 @@ +package rewrite + +import ( + "context" + "reflect" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +type MockedUpstream struct{} + +func (u *MockedUpstream) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + m := new(dns.Msg) + m.SetReply(state.Req) + m.Authoritative = true + switch state.Req.Question[0].Name { + case "xyz.example.com.": + m.Answer = []dns.RR{ + test.A("xyz.example.com. 3600 IN A 3.4.5.6"), + } + return m, nil + case "bard.google.com.cdn.cloudflare.net.": + m.Answer = []dns.RR{ + test.A("bard.google.com.cdn.cloudflare.net. 1800 IN A 9.7.2.1"), + } + return m, nil + case "www.hosting.xyz.": + m.Answer = []dns.RR{ + test.A("www.hosting.xyz. 500 IN A 20.30.40.50"), + } + return m, nil + case "abcd.zzzz.www.pqrst.": + m.Answer = []dns.RR{ + test.A("abcd.zzzz.www.pqrst. 120 IN A 101.20.5.1"), + test.A("abcd.zzzz.www.pqrst. 120 IN A 101.20.5.2"), + } + return m, nil + case "orders.webapp.eu.org.": + m.Answer = []dns.RR{ + test.A("orders.webapp.eu.org. 120 IN A 20.0.0.9"), + } + return m, nil + } + return &dns.Msg{}, nil +} + +func TestCNameTargetRewrite(t *testing.T) { + rules := []Rule{} + ruleset := []struct { + args []string + expectedType reflect.Type + }{ + {[]string{"continue", "cname", "exact", "def.example.com.", "xyz.example.com."}, reflect.TypeOf(&cnameTargetRule{})}, + {[]string{"continue", "cname", "prefix", "chat.openai.com", "bard.google.com"}, reflect.TypeOf(&cnameTargetRule{})}, + {[]string{"continue", "cname", "suffix", "uvw.", "xyz."}, reflect.TypeOf(&cnameTargetRule{})}, + {[]string{"continue", "cname", "substring", "efgh", "zzzz.www"}, reflect.TypeOf(&cnameTargetRule{})}, + {[]string{"continue", "cname", "regex", `(.*)\.web\.(.*)\.site\.`, `{1}.webapp.{2}.org.`}, reflect.TypeOf(&cnameTargetRule{})}, + } + for i, r := range ruleset { + rule, err := newRule(r.args...) + if err != nil { + t.Fatalf("Rule %d: FAIL, %s: %s", i, r.args, err) + } + if reflect.TypeOf(rule) != r.expectedType { + t.Fatalf("Rule %d: FAIL, %s: rule type mismatch, expected %q, but got %q", i, r.args, r.expectedType, rule) + } + cnameTargetRule := rule.(*cnameTargetRule) + cnameTargetRule.Upstream = &MockedUpstream{} + rules = append(rules, rule) + } + doTestCNameTargetTests(rules, t) +} + +func doTestCNameTargetTests(rules []Rule, t *testing.T) { + tests := []struct { + from string + fromType uint16 + answer []dns.RR + expectedAnswer []dns.RR + }{ + {"abc.example.com", dns.TypeA, + []dns.RR{ + test.CNAME("abc.example.com. 5 IN CNAME def.example.com."), + test.A("def.example.com. 5 IN A 1.2.3.4"), + }, + []dns.RR{ + test.CNAME("abc.example.com. 5 IN CNAME xyz.example.com."), + test.A("xyz.example.com. 3600 IN A 3.4.5.6"), + }, + }, + {"chat.openai.com", dns.TypeA, + []dns.RR{ + test.CNAME("chat.openai.com. 20 IN CNAME chat.openai.com.cdn.cloudflare.net."), + test.A("chat.openai.com.cdn.cloudflare.net. 30 IN A 23.2.1.2"), + test.A("chat.openai.com.cdn.cloudflare.net. 30 IN A 24.6.0.8"), + }, + []dns.RR{ + test.CNAME("chat.openai.com. 20 IN CNAME bard.google.com.cdn.cloudflare.net."), + test.A("bard.google.com.cdn.cloudflare.net. 1800 IN A 9.7.2.1"), + }, + }, + {"coredns.io", dns.TypeA, + []dns.RR{ + test.CNAME("coredns.io. 100 IN CNAME www.hosting.uvw."), + test.A("www.hosting.uvw. 200 IN A 7.2.3.4"), + }, + []dns.RR{ + test.CNAME("coredns.io. 100 IN CNAME www.hosting.xyz."), + test.A("www.hosting.xyz. 500 IN A 20.30.40.50"), + }, + }, + {"core.dns.rocks", dns.TypeA, + []dns.RR{ + test.CNAME("core.dns.rocks. 200 IN CNAME abcd.efgh.pqrst."), + test.A("abcd.efgh.pqrst. 100 IN A 200.30.45.67"), + }, + []dns.RR{ + test.CNAME("core.dns.rocks. 200 IN CNAME abcd.zzzz.www.pqrst."), + test.A("abcd.zzzz.www.pqrst. 120 IN A 101.20.5.1"), + test.A("abcd.zzzz.www.pqrst. 120 IN A 101.20.5.2"), + }, + }, + {"order.service.eu", dns.TypeA, + []dns.RR{ + test.CNAME("order.service.eu. 200 IN CNAME orders.web.eu.site."), + test.A("orders.web.eu.site. 50 IN A 10.10.15.1"), + }, + []dns.RR{ + test.CNAME("order.service.eu. 200 IN CNAME orders.webapp.eu.org."), + test.A("orders.webapp.eu.org. 120 IN A 20.0.0.9"), + }, + }, + } + ctx := context.TODO() + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion(tc.from, tc.fromType) + m.Question[0].Qclass = dns.ClassINET + m.Answer = tc.answer + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + } + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rw.ServeDNS(ctx, rec, m) + resp := rec.Msg + if len(resp.Answer) == 0 { + t.Errorf("Test %d: FAIL %s (%d) Expected valid response but received %q", i, tc.from, tc.fromType, resp) + continue + } + if !reflect.DeepEqual(resp.Answer, tc.expectedAnswer) { + t.Errorf("Test %d: FAIL %s (%d) Actual are expected answer does not match, actual: %v, expected: %v", + i, tc.from, tc.fromType, resp.Answer, tc.expectedAnswer) + continue + } + } +} diff --git a/plugin/rewrite/name.go b/plugin/rewrite/name.go index 95d2b7a0d..d3da9c2b8 100644 --- a/plugin/rewrite/name.go +++ b/plugin/rewrite/name.go @@ -92,7 +92,7 @@ type nameRewriterResponseRule struct { stringRewriter } -func (r *nameRewriterResponseRule) RewriteResponse(rr dns.RR) { +func (r *nameRewriterResponseRule) RewriteResponse(res *dns.Msg, rr dns.RR) { rr.Header().Name = r.rewriteString(rr.Header().Name) } @@ -101,7 +101,7 @@ type valueRewriterResponseRule struct { stringRewriter } -func (r *valueRewriterResponseRule) RewriteResponse(rr dns.RR) { +func (r *valueRewriterResponseRule) RewriteResponse(res *dns.Msg, rr dns.RR) { value := getRecordValueForRewrite(rr) if value != "" { new := r.rewriteString(value) diff --git a/plugin/rewrite/reverter.go b/plugin/rewrite/reverter.go index 7abbfb89f..853d96d9d 100644 --- a/plugin/rewrite/reverter.go +++ b/plugin/rewrite/reverter.go @@ -41,7 +41,7 @@ func NewRevertPolicy(noRevert, noRestore bool) RevertPolicy { // ResponseRule contains a rule to rewrite a response with. type ResponseRule interface { - RewriteResponse(rr dns.RR) + RewriteResponse(res *dns.Msg, rr dns.RR) } // ResponseRules describes an ordered list of response rules to apply @@ -91,7 +91,7 @@ func (r *ResponseReverter) WriteMsg(res1 *dns.Msg) error { func (r *ResponseReverter) rewriteResourceRecord(res *dns.Msg, rr dns.RR) { for _, rule := range r.ResponseRules { - rule.RewriteResponse(rr) + rule.RewriteResponse(res, rr) } } diff --git a/plugin/rewrite/rewrite.go b/plugin/rewrite/rewrite.go index b28352cbd..d991c7f02 100644 --- a/plugin/rewrite/rewrite.go +++ b/plugin/rewrite/rewrite.go @@ -139,6 +139,8 @@ func newRule(args ...string) (Rule, error) { return newEdns0Rule(mode, args[startArg:]...) case "ttl": return newTTLRule(mode, args[startArg:]...) + case "cname": + return newCNAMERule(mode, args[startArg:]...) default: return nil, fmt.Errorf("invalid rule type %q", args[0]) } diff --git a/plugin/rewrite/ttl.go b/plugin/rewrite/ttl.go index 1791301d6..5430fc923 100644 --- a/plugin/rewrite/ttl.go +++ b/plugin/rewrite/ttl.go @@ -18,7 +18,7 @@ type ttlResponseRule struct { maxTTL uint32 } -func (r *ttlResponseRule) RewriteResponse(rr dns.RR) { +func (r *ttlResponseRule) RewriteResponse(res *dns.Msg, rr dns.RR) { if rr.Header().Ttl < r.minTTL { rr.Header().Ttl = r.minTTL } else if rr.Header().Ttl > r.maxTTL { |