Files
coredns/plugin/rewrite/name_metadata.go
houyuwushang 1550b867f7 plugin/rewrite: support opt-in metadata in regex name replacements (#8665)
* plugin/rewrite: support opt-in metadata in regex name replacements

Signed-off-by: houyuwushang <180804215+houyuwushang@users.noreply.github.com>

* test: separate CoreDNS and third-party imports

Signed-off-by: houyuwushang <180804215+houyuwushang@users.noreply.github.com>

---------

Signed-off-by: houyuwushang <180804215+houyuwushang@users.noreply.github.com>
2026-10-19 06:54:15 -04:00

108 lines
2.9 KiB
Go

package rewrite
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// metadataNameRule expands a replacement compiled at setup time. Values from
// captures and metadata are inserted literally, without interpreting them again.
type metadataNameRule struct {
nameRuleBase
pattern *regexp.Regexp
parts []nameReplacementPart
}
type nameReplacementPart struct {
literal string
label string
group int // -1 for literals and metadata labels
}
func newMetadataNameRule(nextAction string, auto bool, from, to string, answers ResponseRules) (Rule, error) {
pattern, err := isValidRegexPattern(from, "")
if err != nil {
return nil, err
}
parts, err := parseNameReplacement(dns.Fqdn(to), pattern.NumSubexp())
if err != nil {
return nil, err
}
return &metadataNameRule{
nameRuleBase: newNameRuleBase(nextAction, auto, "", answers),
pattern: pattern,
parts: parts,
}, nil
}
func parseNameReplacement(replacement string, groups int) ([]nameReplacementPart, error) {
var parts []nameReplacementPart
for replacement != "" {
before, rest, found := strings.Cut(replacement, "{")
if strings.Contains(before, "}") {
return nil, fmt.Errorf("unmatched closing brace in metadata name replacement")
}
if before != "" {
parts = append(parts, nameReplacementPart{literal: strings.ToLower(before), group: -1})
}
if !found {
break
}
label, after, closed := strings.Cut(rest, "}")
if !closed {
return nil, fmt.Errorf("unclosed placeholder in metadata name replacement")
}
part := nameReplacementPart{group: -1}
if group, err := strconv.Atoi(label); err == nil {
if group < 0 || group > groups {
return nil, fmt.Errorf("capture group %s does not exist in name matching pattern", label)
}
part.group = group
} else {
if !metadata.IsLabel(label) || strings.ContainsAny(label, "{}") {
return nil, fmt.Errorf("invalid metadata label %q in name replacement", label)
}
part.label = label
}
parts = append(parts, part)
replacement = after
}
return parts, nil
}
func (rule *metadataNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) {
groups := rule.pattern.FindStringSubmatch(state.Name())
if len(groups) == 0 {
return nil, RewriteIgnored
}
var replacement strings.Builder
for _, part := range rule.parts {
switch {
case part.group >= 0:
replacement.WriteString(groups[part.group])
case part.label != "":
value := metadata.ValueFunc(ctx, part.label)
if value == nil {
return nil, RewriteIgnored
}
text := value()
if text == "" {
return nil, RewriteIgnored
}
replacement.WriteString(strings.ToLower(text))
default:
replacement.WriteString(part.literal)
}
}
state.Req.Question[0].Name = replacement.String()
return rule.responseRuleFor(state)
}