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

Add support for extended error codes in blocklist-v2 #373

Merged
merged 4 commits into from
Apr 21, 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
7 changes: 7 additions & 0 deletions blocklist.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type BlocklistOptions struct {

// Refresh period for the allowlist. Disabled if 0.
AllowlistRefresh time.Duration

// Optional, allows specifying extended errors to be used in the
// response when blocking.
EDNS0EDETemplate *EDNS0EDETemplate
}

type BlocklistMetrics struct {
Expand Down Expand Up @@ -172,6 +176,9 @@ func (r *Blocklist) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {

// Block the request with NXDOMAIN if there was a match but no valid spoofed IP is given
log.Debug("blocking request")
if err := r.EDNS0EDETemplate.Apply(answer, q); err != nil {
log.WithError(err).Error("failed to apply edns0ede template")
}
answer.SetRcode(q, dns.RcodeNameError)
return answer, nil
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/routedns/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ type group struct {
NS []string
Extra []string
RCode int
EDNS0EDE *struct {
EDNS0EDE struct {
Code uint16 `toml:"code"` // Code defined in https://datatracker.ietf.org/doc/html/rfc8914
Text string `toml:"text"` // Extra text containing additional information
} `toml:"edns0-ede"` // Extended DNS Errors
Expand Down
19 changes: 19 additions & 0 deletions cmd/routedns/example-config/blocklist-domain-ede.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[resolvers.cloudflare-dot]
address = "1.1.1.1:853"
protocol = "dot"

[groups.cloudflare-blocklist]
type = "blocklist-v2"
resolvers = ["cloudflare-dot"] # Anything that passes the filter is sent on to this resolver
blocklist-format = "domain" # "domain", "hosts" or "regexp", defaults to "regexp"
edns0-ede = {code = 15, text = "Blocked {{ .Question }} with ID {{ .ID }} because reasons "} # Extended error code
blocklist = [ # Define the names to be blocked
'evil.com',
'.facebook.com',
'*.twitter.com',
]

[listeners.local-udp]
address = ":53"
protocol = "udp"
resolver = "cloudflare-blocklist"
32 changes: 19 additions & 13 deletions cmd/routedns/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
syslog "github.com/RackSec/srslog"
rdns "github.com/folbricht/routedns"
"github.com/heimdalr/dag"
"github.com/miekg/dns"
"github.com/redis/go-redis/v9"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -413,13 +412,18 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
return err
}
}
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.BlocklistOptions{
BlocklistResolver: resolvers[g.BlockListResolver],
BlocklistDB: blocklistDB,
BlocklistRefresh: time.Duration(g.BlocklistRefresh) * time.Second,
AllowListResolver: resolvers[g.AllowListResolver],
AllowlistDB: allowlistDB,
AllowlistRefresh: time.Duration(g.AllowlistRefresh) * time.Second,
EDNS0EDETemplate: edeTpl,
}
resolvers[id], err = rdns.NewBlocklist(id, gr[0], opt)
if err != nil {
Expand Down Expand Up @@ -658,12 +662,17 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
return err
}
}
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.ResponseBlocklistIPOptions{
BlocklistResolver: resolvers[g.BlockListResolver],
BlocklistDB: blocklistDB,
BlocklistRefresh: time.Duration(g.BlocklistRefresh) * time.Second,
Filter: g.Filter,
Inverted: g.Inverted,
EDNS0EDETemplate: edeTpl,
}
resolvers[id], err = rdns.NewResponseBlocklistIP(id, gr[0], opt)
if err != nil {
Expand Down Expand Up @@ -744,20 +753,17 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
}

case "static-responder":
var edns0Options []dns.EDNS0
if g.EDNS0EDE != nil {
edns0Options = append(edns0Options, &dns.EDNS0_EDE{
InfoCode: g.EDNS0EDE.Code,
ExtraText: g.EDNS0EDE.Text,
})
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,
EDNS0Options: edns0Options,
Answer: g.Answer,
NS: g.NS,
Extra: g.Extra,
RCode: g.RCode,
Truncate: g.Truncate,
EDNS0EDETemplate: edeTpl,
}
resolvers[id], err = rdns.NewStaticResolver(id, opt)
if err != nil {
Expand Down
29 changes: 22 additions & 7 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
- [Random group](#Random-group)
- [Fastest group](#Fastest-group)
- [Replace](#Replace)
- [Query Blocklist](#Query-Blocklist)
- [Query Blocklist](#query-blocklist)
- [Response Blocklist](#Response-Blocklist)
- [Client Blocklist](#Client-Blocklist)
- [EDNS0 Client Subnet modifier](#EDNS0-Client-Subnet-Modifier)
Expand All @@ -45,6 +45,7 @@
- [DNS-over-QUIC](#DNS-over-QUIC-Resolver)
- [Bootstrap Resolver](#Bootstrap-Resolver)
- [SOCKS5 Proxy Support](#SOCKS5-Proxy-Support)
- [Templates](#templates)

## Overview

Expand Down Expand Up @@ -601,6 +602,7 @@ Options:
- `allowlist-format` - The format the allowlist is provided in. Only used if `allowlist-source` is not provided. Can be `regexp`, `domain`, or `hosts`. Defaults to `regexp`.
- `allowlist-refresh` - Time interval (in seconds) in which external allowlists are reloaded. Optional.
- `allowlist-source` - An array of allowlists, each with `format`, `source`, and optionally `cache-dir` or `allow-failure`.
- `edns0-ede` - Optional, include an extended error code in the response if it's blocked. Only used when the response is blocked, not when it's spoofed. The value is a struct with two keys, `code` (number) and `text` (string). Possible values for `code` are defined in [rfc8914](https://datatracker.ietf.org/doc/html/rfc8914) while `text` can carry additional information that is displayed by `dig` for example. The `text` value is a template that has access to a number of fields of query to allow customizing the response based on data in the query. See [Templates](#templates) for details. Simple placeholders in `text` would be `{{ .Question }}` for the question in the query or `{{ .ID }}` to be replaced with the query ID.

When using the `cache-dir` option on a list that loads rules via HTTP, the results are cached into a file in the given directory. The filename is the URL of the source hashed with SHA256 so multiple blocklists can be cached in the same directory. If a cached file exists on startup, it is used instead of refreshing the list from the remote location (slowing down startup).

Expand All @@ -621,13 +623,14 @@ blocklist = [ # Define the names to be blocked
]
```

Simple blocklist with static `domain`-format rule in the configuration.
Simple blocklist with static `domain`-format rule in the configuration. This will respond with an extended error code and a message containing the question name.

```toml
[groups.my-blocklist]
type = "blocklist-v2"
resolvers = ["upstream-resolver"]
bloclist-format = "domain"
type = "blocklist-v2"
resolvers = ["upstream-resolver"]
blocklist-format = "domain"
edns0-ede = {code = 15, text = "Blocked {{ .Question }}"}
blocklist = [
'domain1.com', # Exact match
'.domain2.com', # Exact match and all sub-domains
Expand Down Expand Up @@ -691,7 +694,7 @@ allowlist-source = [
]
```

Example config files: [blocklist-regexp.toml](../cmd/routedns/example-config/blocklist-regexp.toml), [block-split-cache.toml](../cmd/routedns/example-config/block-split-cache.toml), [blocklist-domain.toml](../cmd/routedns/example-config/blocklist-domain.toml), [blocklist-hosts.toml](../cmd/routedns/example-config/blocklist-hosts.toml), [blocklist-local.toml](../cmd/routedns/example-config/blocklist-local.toml), [blocklist-remote.toml](../cmd/routedns/example-config/blocklist-remote.toml), [blocklist-allow.toml](../cmd/routedns/example-config/blocklist-allow.toml), [blocklist-resolver.toml](../cmd/routedns/example-config/blocklist-resolver.toml)
Example config files: [blocklist-regexp.toml](../cmd/routedns/example-config/blocklist-regexp.toml), [block-split-cache.toml](../cmd/routedns/example-config/block-split-cache.toml), [blocklist-domain.toml](../cmd/routedns/example-config/blocklist-domain.toml), [blocklist-hosts.toml](../cmd/routedns/example-config/blocklist-hosts.toml), [blocklist-local.toml](../cmd/routedns/example-config/blocklist-local.toml), [blocklist-remote.toml](../cmd/routedns/example-config/blocklist-remote.toml), [blocklist-allow.toml](../cmd/routedns/example-config/blocklist-allow.toml), [blocklist-resolver.toml](../cmd/routedns/example-config/blocklist-resolver.toml), [blocklist-domain-ede.toml](../cmd/routedns/example-config/blocklist-domain-ede.toml)

### Response Blocklist

Expand All @@ -718,6 +721,7 @@ Options:
- `filter` - If set to `true` in `response-blocklist-ip`, matching records will be removed from responses rather than the whole response. If there is no answer record left after applying the filter, NXDOMAIN will be returned unless an alternative `blocklist-resolver` is defined.
- `inverted` - Inverts the behavior of the blocklist. If set to `true`, only IPs that are on the blocklist are allowed and responses containing an IP not on the blocklist are blocked. Can be combined with `filter` to remove any IPs not on the blocklist from the response.
- `location-db` - If location-based IP blocking is used, this specifies the GeoIP data file to load. Optional. Defaults to /usr/share/GeoIP/GeoLite2-City.mmdb
- `edns0-ede` - Optional, include an extended error code in the response if it's blocked. Only used when the response is blocked, not when it's spoofed. The value is a struct with two keys, `code` (number) and `text` (string). Possible values for `code` are defined in [rfc8914](https://datatracker.ietf.org/doc/html/rfc8914) while `text` can carry additional information that is displayed by `dig` for example. The `text` value is a template that has access to a number of fields of query to allow customizing the response based on data in the query. See [Templates](#templates) for details. Simple placeholders in `text` would be `{{ .Question }}` for the question in the query or `{{ .ID }}` to be replaced with the query ID.

Location-based blocking requires a list of GeoName IDs of geographical entities (Continent, Country, City or Subdivision) and the GeoName ID, like `2750405` for Netherlands. The GeoName ID can be looked up in [https://www.geonames.org/](https://www.geonames.org/). Locations are read from a MAXMIND GeoIP2 database that either has to be present in `/usr/share/GeoIP/GeoLite2-City.mmdb` or is configured with the `location-db` option.

Expand Down Expand Up @@ -947,8 +951,8 @@ Options:
- `answer` - Array of strings, each one representing a line in zone-file format. Forms the content of the Answer records in the response. The name in all answer records is replaced with the name in the query to create a match.
- `ns` - Array of strings, each one representing a line in zone-file format. Forms the content of the Authority records in the response.
- `extra` - Array of strings, each one representing a line in zone-file format. Forms the content of the Additional records in the response.
- `edns0-ede` - Include an extended error code in the response. It's a struct with two keys, `code` (number) and `text` (string). Possible values for `code` are defined in [rfc8914](https://datatracker.ietf.org/doc/html/rfc8914) while `text` can carry additional information that is displayed by `dig` for example.
- `truncate` - when true, TC Bit is set in response. Default is false.
- `edns0-ede` - Optional, include an extended error code in the response if it's blocked. Only used when the response is blocked, not when it's spoofed. The value is a struct with two keys, `code` (number) and `text` (string). Possible values for `code` are defined in [rfc8914](https://datatracker.ietf.org/doc/html/rfc8914) while `text` can carry additional information that is displayed by `dig` for example. The `text` value is a template that has access to a number of fields of query to allow customizing the response based on data in the query. See [Templates](#templates) for details. Simple placeholders in `text` would be `{{ .Question }}` for the question in the query or `{{ .ID }}` to be replaced with the query ID.

Note:

Expand Down Expand Up @@ -1615,3 +1619,14 @@ socks5-address = "1.2.3.4:1080"
socks5-username = "test"
socks5-password = "test"
```

## Templates

Some groups support templates, i.e. allow placeholder in text fields that will be populated at runtime with data from a query. This can for example be used in the extended error text returned from a blocklist. In that case, the configuration would set a text with placeholders like this `"Blocked {{ .Question }} with ID {{ .ID }} because reasons"`. The placeholders in between `{{` and `}}` would then be replaced with data from the query when a query is blocked and the response returned. The template syntax is explained in more detail [here](https://pkg.go.dev/text/template).

**Data available to templates**

The following pieces of information from the query are available in the template:

- `ID` - The query ID.
- `Question` - The question string.
68 changes: 68 additions & 0 deletions edns0ede.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package rdns

import (
"bytes"
"text/template"

"github.com/miekg/dns"
)

type EDNS0EDETemplate struct {
infoCode uint16
extraText string
textTemplate *template.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)
if err != nil {
return nil, err
}

return &EDNS0EDETemplate{
infoCode: infoCode,
extraText: extraText,
textTemplate: textTemplate,
}, 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 {
return err
}

ede := &dns.EDNS0_EDE{
InfoCode: t.infoCode,
ExtraText: text.String(),
}
msg.SetEdns0(4096, false)
opt := msg.IsEdns0()
opt.Option = append(opt.Option, ede)
return nil
}
12 changes: 10 additions & 2 deletions response-blocklist-ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ type ResponseBlocklistIPOptions struct {
BlocklistRefresh time.Duration

// If true, removes matching records from the response rather than replying with NXDOMAIN. Can
// not be combined with alternative blockist-resolver
// not be combined with alternative blocklist-resolver
Filter bool

// Inverted behavior, only allow responses that can be found on at least one list.
Inverted bool

// Optional, allows specifying extended errors to be used in the
// response when blocking.
EDNS0EDETemplate *EDNS0EDETemplate
}

// NewResponseBlocklistIP returns a new instance of a response blocklist resolver.
Expand Down Expand Up @@ -113,7 +117,11 @@ func (r *ResponseBlocklistIP) blockIfMatch(query, answer *dns.Msg, ci ClientInfo
return r.BlocklistResolver.Resolve(query, ci)
}
log.Debug("blocking response")
return nxdomain(query), nil
answer = nxdomain(query)
if err := r.EDNS0EDETemplate.Apply(answer, query); err != nil {
log.WithError(err).Error("failed to apply edns0ede template")
}
return answer, nil
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion response-blocklist-name.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ type ResponseBlocklistNameOptions struct {

// Inverted behavior, only allow responses that can be found on at least one list.
Inverted bool

// Optional, allows specifying extended errors to be used in the
// response when blocking.
EDNS0EDETemplate *EDNS0EDETemplate
}

// NewResponseBlocklistName returns a new instance of a response blocklist resolver.
Expand Down Expand Up @@ -106,7 +110,11 @@ func (r *ResponseBlocklistName) blockIfMatch(query, answer *dns.Msg, ci ClientIn
return r.BlocklistResolver.Resolve(query, ci)
}
log.Debug("blocking response")
return nxdomain(query), nil
answer = nxdomain(query)
if err := r.EDNS0EDETemplate.Apply(answer, query); err != nil {
log.WithError(err).Error("failed to apply edns0ede template")
}
return answer, nil
}
}
}
Expand Down
Loading
Loading