Files
coredns/plugin/rewrite/cname_target.go
2025-09-02 13:46:47 -07:00

163 lines
5.1 KiB
Go

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
Upstream UpstreamInt // Upstream for looking up external names during the resolution process.
}
// cnameTargetRuleWithReqState is cname target rewrite rule state
type cnameTargetRuleWithReqState struct {
rule cnameTargetRule
state request.Request
ctx context.Context
}
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.ReplaceAll(inputCName, r.paramFromTarget, r.paramToTarget)
}
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.ReplaceAll(substitution, groupIndexStr, groupValue)
}
return inputCName, substitution
}
return "", ""
}
func (r *cnameTargetRuleWithReqState) 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.rule.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 can be nil if the internal query path didn't write a response
// (e.g. a plugin returned a success rcode without writing, dropped the query,
// or the context was canceled). Guard upRes before dereferencing.
upRes, err := r.rule.Upstream.Lookup(r.ctx, r.state, toTarget, r.state.Req.Question[0].Qtype)
if err != nil {
log.Errorf("upstream lookup failed: %v", err)
return
}
if upRes == nil {
log.Errorf("upstream lookup returned nil")
return
}
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 received
for _, rr := range upRes.Answer {
if rr.Header().Name == toTarget {
newAnswer = append(newAnswer, rr)
}
}
res.Answer = newAnswer
// if not propagated, the truncated response might get cached,
// and it will be impossible to resolve the full response
res.Truncated = upRes.Truncated
}
}
}
}
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 r != nil && len(r.rewriteType) > 0 && len(r.paramFromTarget) > 0 && len(r.paramToTarget) > 0 {
return ResponseRules{&cnameTargetRuleWithReqState{
rule: *r,
state: state,
ctx: ctx,
}}, RewriteDone
}
return nil, RewriteIgnored
}
// Mode returns the processing mode.
func (r *cnameTargetRule) Mode() string { return r.nextAction }