Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New static-template group #378

Merged
merged 2 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cmd/routedns/example-config/static-template.toml
Original file line number Diff line number Diff line change
@@ -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',
]
17 changes: 17 additions & 0 deletions cmd/routedns/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,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)
Expand Down
122 changes: 81 additions & 41 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1019,6 +1019,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.
Expand Down Expand Up @@ -1630,3 +1652,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.
33 changes: 6 additions & 27 deletions edns0ede.go
Original file line number Diff line number Diff line change
@@ -1,65 +1,44 @@
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) {
if infoCode == 0 && extraText == "" {
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.
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()
Expand Down
99 changes: 99 additions & 0 deletions static-template.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading