New static-template group (#378)

* New static-template group

* example file
This commit is contained in:
Frank Olbricht
2024-06-22 14:05:22 +02:00
committed by GitHub
parent ef62efb70b
commit ea836d4bc3
6 changed files with 282 additions and 68 deletions

View File

@@ -0,0 +1,18 @@
# Static template responder that builds a response based on data in the query
[listeners.local-udp]
address = "127.0.0.1:53"
protocol = "udp"
resolver = "static"
[groups.static]
type = "static-template"
answer = [
'{{ .Question }} IN {{ .QuestionType }} {{ trimSuffix .Question ".rebind."}}'
]
ns = [
'{{ .Question }} 18000 IN NS ns1.{{ .Question }}',
]
extra = [
'ns1.{{ .Question }} 1800 IN A 127.0.0.1',
]

View File

@@ -771,6 +771,23 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
if err != nil {
return err
}
case "static-template":
edeTpl, err := rdns.NewEDNS0EDETemplate(g.EDNS0EDE.Code, g.EDNS0EDE.Text)
if err != nil {
return fmt.Errorf("failed to parse edn0 template in %q: %w", id, err)
}
opt := rdns.StaticResolverOptions{
Answer: g.Answer,
NS: g.NS,
Extra: g.Extra,
RCode: g.RCode,
Truncate: g.Truncate,
EDNS0EDETemplate: edeTpl,
}
resolvers[id], err = rdns.NewStaticTemplateResolver(id, opt)
if err != nil {
return err
}
case "response-minimize":
if len(gr) != 1 {
return fmt.Errorf("type response-minimize only supports one resolver in '%s'", id)

View File

@@ -2,49 +2,49 @@
## Table of contents
- [Overview](#Overview)
- [Split Configuration](#Split-Configuration)
- [Overview](#overview)
- [Split Configuration](#split-configuration)
- [Regex Formatting](https://github.com/google/re2/wiki/Syntax)
- [Listeners](#Listeners)
- [Plain DNS](#Plain-DNS)
- [DNS-over-TLS](#DNS-over-TLS)
- [DNS-over-HTTPS](#DNS-over-HTTPS)
- [DNS-over-DTLS](#DNS-over-DTLS)
- [DNS-over-QUIC](#DNS-over-QUIC)
- [Admin](#Admin)
- [Modifiers, Groups and Routers](#Modifiers-Groups-and-Routers)
- [Cache](#Cache)
- [TTL Modifier](#TTL-modifier)
- [Round-Robin group](#Round-Robin-group)
- [Fail-Rotate group](#Fail-Rotate-group)
- [Fail-Back group](#Fail-Back-group)
- [Random group](#Random-group)
- [Fastest group](#Fastest-group)
- [Replace](#Replace)
- [Listeners](#listeners)
- [Plain DNS](#plain-dns)
- [DNS-over-TLS](#dns-over-tls)
- [DNS-over-HTTPS](#dns-over-https)
- [DNS-over-DTLS](#dns-over-dtls)
- [DNS-over-QUIC](#dns-over-quic)
- [Admin](#admin)
- [Modifiers, Groups and Routers](#modifiers-groups-and-routers)
- [Cache](#cache)
- [TTL Modifier](#ttl-modifier)
- [Round-Robin group](#round-robin-group)
- [Fail-Rotate group](#fail-rotate-group)
- [Fail-Back group](#fail-back-group)
- [Random group](#random-group)
- [Fastest group](#fastest-group)
- [Replace](#replace)
- [Query Blocklist](#query-blocklist)
- [Response Blocklist](#Response-Blocklist)
- [Client Blocklist](#Client-Blocklist)
- [EDNS0 Client Subnet modifier](#EDNS0-Client-Subnet-Modifier)
- [EDNS0 modifier](#EDNS0-Modifier)
- [Static responder](#Static-responder)
- [Drop](#Drop)
- [Response Minimizer](#Response-Minimizer)
- [Response Collapse](#Response-Collapse)
- [Router](#Router)
- [Rate Limiter](#Rate-Limiter)
- [Rate Limiter](#Rate-Limiter)
- [Fastest TCP Probe](#Fastest-TCP-Probe)
- [Retrying Truncated Responses](#Retrying-Truncated-Responses)
- [Request Deduplication](#Request-Deduplication)
- [Syslog](#Syslog)
- [Resolvers](#Resolvers)
- [Plain DNS](#Plain-DNS-Resolver)
- [DNS-over-TLS](#DNS-over-TLS-Resolver)
- [DNS-over-HTTPS](#DNS-over-HTTPS-Resolver)
- [DNS-over-DTLS](#DNS-over-DTLS-Resolver)
- [DNS-over-QUIC](#DNS-over-QUIC-Resolver)
- [Bootstrap Resolver](#Bootstrap-Resolver)
- [SOCKS5 Proxy Support](#SOCKS5-Proxy-Support)
- [Response Blocklist](#response-blocklist)
- [Client Blocklist](#client-blocklist)
- [EDNS0 Client Subnet modifier](#edns0-client-subnet-modifier)
- [EDNS0 modifier](#edns0-modifier)
- [Static Responder](#static-responder)
- [Static Template Responder](#static-template-responder)
- [Drop](#drop)
- [Response Minimizer](#response-minimizer)
- [Response Collapse](#response-collapse)
- [Router](#router)
- [Rate Limiter](#rate-limiter)
- [Fastest TCP Probe](#fastest-tcp-probe)
- [Retrying Truncated Responses](#retrying-truncated-responses)
- [Request Deduplication](#request-deduplication)
- [Syslog](#syslog)
- [Resolvers](#resolvers)
- [Plain DNS](#plain-dns-resolver)
- [DNS-over-TLS](#dns-over-tls-resolver)
- [DNS-over-HTTPS](#dns-over-https-resolver)
- [DNS-over-DTLS](#dns-over-dtls-resolver)
- [DNS-over-QUIC](#dns-over-quic-resolver)
- [Bootstrap Resolver](#bootstrap-resolver)
- [SOCKS5 Proxy Support](#socks5-proxy-support)
- [Templates](#templates)
## Overview
@@ -1020,6 +1020,28 @@ edns0-ede = {code = 15, text = "Blocked because reasons"}
Example config files: [walled-garden.toml](../cmd/routedns/example-config/walled-garden.toml), [rfc8482.toml](../cmd/routedns/example-config/rfc8482.toml), [static-extended-error.toml](../cmd/routedns/example-config/static-extended-error.toml)
### Static Template Responder
A static template responder operates similarly to a [Static Responder](#static-responder) with the main difference being that the records configured are templates, meaning they can contain placeholders which can refer to data in the query, such as the question. Based on the values in the question, the template can manipulate the response. Templates can contain more complex operations such as string splitting, replacing etc.
#### Configuration
See [Static Responder](#static-responder) for a list of options. The values are the same except that the string values are treated as [templates](#templates).
Examples:
A fixed responder that can respond to queries like `192.168.1.12.rebind.` by striping the `.rebind.` suffix and treating the remaining string as IP. Note that the template in this case has to produce a valid IP or it will fail. To ensure the queries reaching this responder are always valid it may be best to combine with a router or blocklist in front of it.
```toml
[groups.static]
type = "static-template"
answer = [
'{{ .Question }} IN A {{ trimSuffix .Question ".rebind."}}'
]
```
Example config files: [static-template.toml](../cmd/routedns/example-config/static-template.toml)
### Drop
Terminates a pipeline by dropping the request. Typically used with blocklists to abort queries that match block rules. UDP and TCP listeners close the connection without replying, while HTTP listeners will reply with an HTTP error.
@@ -1635,3 +1657,21 @@ The following pieces of information from the query are available in the template
- `ID` - The query ID.
- `Question` - The question string.
- `QuestionType` - The question type, `A`, `AAAA`, `CNAME` etc.
- `QuestionClass` - The query class, `IN`, `ANY`, etc.
In addition to the [built-in template functions](https://pkg.go.dev/text/template#hdr-Functions), the following functions are available.
- `replaceAll` - Replace all instances of a substring with another. Equivalent to [strings.ReplaceAll](https://pkg.go.dev/strings#ReplaceAll)
- `trimPrefix` - Removes a prefix from string. Equivalent to [strings.TrimPrefix](https://pkg.go.dev/strings#TrimPrefix).
- `trimSuffix` - Removes a suffix from a string. Equivalent to [strings.TrimSuffix](https://pkg.go.dev/strings#TrimPrefix).
- `split` - Split strings into substrings using the given separator. Equivalent to [strings.Split](https://pkg.go.dev/strings#Split).
- `join` - Concatenates strings with a given separator. Equivalent to [strings.Join](https://pkg.go.dev/strings#Join).
Functions can be combined with conditionals to make more complex template such as this example.
```template
'{{ .Question }} 18000 IN NS {{ if (eq .QuestionType "AAAA") }}ns6{{ else }}ns4{{ end }}.example.com.'
```
Support for additional string-manipulation functions can be added as needed.

View File

@@ -1,16 +1,12 @@
package rdns
import (
"bytes"
"text/template"
"github.com/miekg/dns"
)
type EDNS0EDETemplate struct {
infoCode uint16
extraText string
textTemplate *template.Template
textTemplate *Template
}
func NewEDNS0EDETemplate(infoCode uint16, extraText string) (*EDNS0EDETemplate, error) {
@@ -18,25 +14,17 @@ func NewEDNS0EDETemplate(infoCode uint16, extraText string) (*EDNS0EDETemplate,
return nil, nil
}
textTemplate := template.New("EDNS0EDE")
textTemplate, err := textTemplate.Parse(extraText)
tpl, err := NewTemplate(extraText)
if err != nil {
return nil, err
}
return &EDNS0EDETemplate{
infoCode: infoCode,
extraText: extraText,
textTemplate: textTemplate,
textTemplate: tpl,
}, nil
}
// Data that is passed to any templates.
type templateInput struct {
ID uint16
Question string
}
// Apply executes the template for the EDNS0-EDE record text, e.g. replacing
// placeholders in the Text with Query names, then adding the EDE record to
// the given msg.
@@ -44,22 +32,13 @@ func (t *EDNS0EDETemplate) Apply(msg, q *dns.Msg) error {
if t == nil {
return nil
}
var question string
if len(q.Question) > 0 {
question = q.Question[0].Name
}
input := templateInput{
ID: q.Id,
Question: question,
}
text := new(bytes.Buffer)
if err := t.textTemplate.Execute(text, input); err != nil {
extraText, err := t.textTemplate.Apply(q)
if err != nil {
return err
}
ede := &dns.EDNS0_EDE{
InfoCode: t.infoCode,
ExtraText: text.String(),
ExtraText: extraText,
}
msg.SetEdns0(4096, false)
opt := msg.IsEdns0()

99
static-template.go Normal file
View File

@@ -0,0 +1,99 @@
package rdns
import (
"github.com/miekg/dns"
)
// StaticTemplateResolver is a resolver that always returns a predefined set of records
// which can be customized with information from the question. It is similar to
// StaticResolver but allows the use of templates with placeholders as input.
type StaticTemplateResolver struct {
id string
answer []*Template
ns []*Template
extra []*Template
rcode int
truncate bool
opt StaticResolverOptions
}
var _ Resolver = &StaticTemplateResolver{}
// NewStaticTemplateResolver returns a new instance of a StaticTemplateResolver resolver.
func NewStaticTemplateResolver(id string, opt StaticResolverOptions) (*StaticTemplateResolver, error) {
r := &StaticTemplateResolver{id: id, opt: opt}
for _, record := range opt.Answer {
tpl, err := NewTemplate(record)
if err != nil {
return nil, err
}
r.answer = append(r.answer, tpl)
}
for _, record := range opt.NS {
tpl, err := NewTemplate(record)
if err != nil {
return nil, err
}
r.ns = append(r.ns, tpl)
}
for _, record := range opt.Extra {
tpl, err := NewTemplate(record)
if err != nil {
return nil, err
}
r.extra = append(r.extra, tpl)
}
r.rcode = opt.RCode
r.truncate = opt.Truncate
return r, nil
}
// Resolve a DNS query by incorporating data from the query into a fixed response.
func (r *StaticTemplateResolver) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
answer := new(dns.Msg)
answer.SetReply(q)
log := logger(r.id, q, ci)
answer.Answer = r.processRRTemplates(q, ci, r.answer...)
answer.Ns = r.processRRTemplates(q, ci, r.ns...)
answer.Extra = r.processRRTemplates(q, ci, r.extra...)
answer.Rcode = r.rcode
answer.Truncated = r.truncate
if err := r.opt.EDNS0EDETemplate.Apply(answer, q); err != nil {
log.WithError(err).Error("failed to apply edns0ede template")
}
logger(r.id, q, ci).WithField("truncated", r.truncate).Debug("responding")
return answer, nil
}
func (r *StaticTemplateResolver) String() string {
return r.id
}
func (r *StaticTemplateResolver) processRRTemplates(q *dns.Msg, ci ClientInfo, templates ...*Template) []dns.RR {
log := logger(r.id, q, ci)
resp := make([]dns.RR, 0, len(templates))
for _, tpl := range templates {
text, err := tpl.Apply(q)
if err != nil {
log.WithError(err).Error("failed to apply template")
continue
}
rr, err := dns.NewRR(text)
if err != nil {
log.WithError(err).Error("failed to parse template output")
continue
}
// Update the name of every answer record to match that of the query
// rr.Header().Name = qName(q)
resp = append(resp, rr)
}
return resp
}

61
template.go Normal file
View File

@@ -0,0 +1,61 @@
package rdns
import (
"bytes"
"strings"
"text/template"
"github.com/miekg/dns"
)
type Template struct {
textTemplate *template.Template
}
func NewTemplate(text string) (*Template, error) {
funcMap := template.FuncMap{
"replaceAll": strings.ReplaceAll,
"trimPrefix": strings.TrimPrefix,
"trimSuffix": strings.TrimSuffix,
"split": strings.Split,
"join": strings.Join,
}
textTemplate := template.New("template").Funcs(funcMap)
textTemplate, err := textTemplate.Parse(text)
if err != nil {
return nil, err
}
return &Template{
textTemplate: textTemplate,
}, nil
}
// Data that is passed to any templates.
type templateInput struct {
ID uint16
Question string
QuestionClass string
QuestionType string
}
// Apply executes the template, e.g. replacing placeholders in the text
// with values from the Query.
func (t *Template) Apply(q *dns.Msg) (string, error) {
if t == nil {
return "", nil
}
var question dns.Question
if len(q.Question) > 0 {
question = q.Question[0]
}
input := templateInput{
ID: q.Id,
Question: question.Name,
QuestionClass: dns.ClassToString[question.Qclass],
QuestionType: dns.TypeToString[question.Qtype],
}
text := new(bytes.Buffer)
err := t.textTemplate.Execute(text, input)
return text.String(), err
}