[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>
This commit is contained in:
Amila Senadheera
2023-04-13 17:49:36 +05:30
committed by GitHub
parent 0063d7a80c
commit 8e8231d627
7 changed files with 362 additions and 5 deletions

View File

@@ -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.

View File

@@ -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 }

View File

@@ -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
}
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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])
}

View File

@@ -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 {