aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
authorGravatar Amila Senadheera <amilaruk1995@gmail.com> 2023-04-13 17:49:36 +0530
committerGravatar GitHub <noreply@github.com> 2023-04-13 08:19:36 -0400
commit8e8231d627a690415da3df7b464cf45a00c0defa (patch)
tree8dd16dea8f363ed59849f1fff1f826f45ede8476 /plugin
parent0063d7a80c0db18069429c775e4b95a5c0b4b69c (diff)
downloadcoredns-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.md47
-rw-r--r--plugin/rewrite/cname_target.go145
-rw-r--r--plugin/rewrite/cname_target_test.go163
-rw-r--r--plugin/rewrite/name.go4
-rw-r--r--plugin/rewrite/reverter.go4
-rw-r--r--plugin/rewrite/rewrite.go2
-rw-r--r--plugin/rewrite/ttl.go2
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 {