Add static-responder group (#25)

* Add static-responder group

* Missing files

* Fix example file
This commit is contained in:
Frank Olbricht
2020-05-17 08:22:00 -06:00
committed by GitHub
parent 36018e4e43
commit b38e459884
7 changed files with 229 additions and 2 deletions

View File

@@ -399,6 +399,37 @@ ecs-prefix4 = 8
ecs-prefix6 = 64
```
## Static responders
A `static-responder` can be used to terminate a query with a fixed answer. The answer can contain Answer, NS, and Extra records with a configurable RCode. Static responsers are useful in combination with routers to build walled-gardens or blocklists providing more control over the response. The individual records in the response are defined in zone-file format. The default TTL is 1h unless given in the record.
A fixed responder that will return a full answer with NS and Extra records with different TTL. The name string in answer records gets updated dynamically to match the query, while NS and Extra records are return unmodified.
```toml
[groups.static-a]
type = "static-responder"
rcode = 0 # Response code: 0 = NOERROR, 1 = FORMERR, 2 = SERVFAIL, 3 = NXDOMAIN, ...
answer = ["IN A 1.2.3.4"]
ns = [
"domain.com. 18000 IN NS ns1.domain.com.",
"domain.com. 18000 IN NS ns2.domain.com.",
]
extra = [
"ns1.domain.com. 1800 IN A 127.0.0.1",
"ns1.domain.com. 1800 IN AAAA ::1",
"ns2.domain.com. 1800 IN A 127.0.0.1",
"ns2.domain.com. 1800 IN AAAA ::1",
]
```
Simple responder that'll reply with SERVFAIL to every query routed to it.
```toml
[groups.static-nxdomain]
type = "static-responder"
rcode = 3
```
## Use-cases / Examples
### Use case 1: Use DNS-over-TLS for all queries locally

View File

@@ -70,6 +70,12 @@ type group struct {
AllowlistFormat string `toml:"allowlist-format"` // only used for static allowlists in the config
AllowlistSource []list `toml:"allowlist-source"`
AllowlistRefresh int `toml:"allowlist-refresh"`
// Static responder options
Answer []string
NS []string
Extra []string
RCode int
}
// Block/Allowlist items for blocklist-v2

View File

@@ -2,7 +2,10 @@
# queries that match the allowlist or blocklist. The default behavior is to
# return NXDOMAIN for any item that matches the blocklist but not the allowlist.
# With resolvers, it's possible to send queries to different resolvers based
# on them matching the allowlist or blocklist. Similar to a router.
# on them matching the allowlist or blocklist, similar to a router. This is
# useful when the blocklist should return something other than NXDOMAIN, the
# query can be routed to a statis-responder instead, providing more control
# over what is returned.
[resolvers.cloudflare-dot]
address = "1.1.1.1:853"
@@ -12,10 +15,15 @@ protocol = "dot"
address = "8.8.8.8:853"
protocol = "dot"
# Returns servfail for every query routed here by the blocklist
[groups.static-servfail]
type = "static-responder"
rcode = 2 # Response code: 0 = NOERROR, 1 = FORMERR, 2 = SERVFAIL, 3 = NXDOMAIN, ...
[groups.cloudflare-blocklist]
type = "blocklist-v2"
resolvers = ["cloudflare-dot"] # Default upstream resolver for anything not matching either list
blocklist-resolver = "google-dot" # Resolver used for anything matching the blocklist (and not on the allowlist)
blocklist-resolver = "static-servfail" # Resolver used for anything matching the blocklist (and not on the allowlist)
blocklist-format = "domain"
blocklist = [
'evil.com',

View File

@@ -0,0 +1,53 @@
# This example shows how to combine routers with static responders to build
# a walled garden. Static responders can also be used with alternative resolvers
# on blocklists to control the block response (something other than NXDOMAIN).
# A static-responder always returns the same response records defined here. The only
# thing that is changed is the name in the answer records to match that of the query.
# It's possible to define Answer, NS, and Extra records in zone-file format as well
# as a RCode which defaults to 0 (NOERROR).
[groups.static-a]
type = "static-responder"
rcode = 0 # Response code: 0 = NOERROR, 1 = FORMERR, 2 = SERVFAIL, 3 = NXDOMAIN, ...
answer = ["IN A 1.2.3.4"]
ns = [
"domain.com. 18000 IN NS ns1.domain.com.",
"domain.com. 18000 IN NS ns2.domain.com.",
]
extra = [
"ns1.domain.com. 1800 IN A 127.0.0.1",
"ns1.domain.com. 1800 IN AAAA ::1",
"ns2.domain.com. 1800 IN A 127.0.0.1",
"ns2.domain.com. 1800 IN AAAA ::1",
]
[groups.static-aaaa]
type = "static-responder"
answer = ["IN AAAA ::1"]
[groups.static-mx]
type = "static-responder"
answer = ["IN MX 10 smtp.acme.test."]
[groups.static-nxdomain]
type = "static-responder"
rcode = 3
# Use a router to send certain types of queries to static responders.
[routers.walled-garden-router]
routes = [
{ type = "A", resolver="static-a" },
{ type = "AAAA", resolver="static-aaaa" },
{ type = "MX", resolver="static-mx" },
{ resolver="static-nxdomain" }, # default route
]
[listeners.local-udp]
address = ":53"
protocol = "udp"
resolver = "walled-garden-router"
[listeners.local-tcp]
address = ":53"
protocol = "tcp"
resolver = "walled-garden-router"

View File

@@ -422,6 +422,18 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
if err != nil {
return err
}
case "static-responder":
opt := rdns.StaticResolverOptions{
Answer: g.Answer,
NS: g.NS,
Extra: g.Extra,
RCode: g.RCode,
}
resolvers[id], err = rdns.NewStaticResolver(opt)
if err != nil {
return err
}
default:
return fmt.Errorf("unsupported group type '%s' for group '%s'", g.Type, id)
}

80
static.go Normal file
View File

@@ -0,0 +1,80 @@
package rdns
import (
"fmt"
"github.com/miekg/dns"
)
// StaticResolver is a resolver that always returns the same answer, to any question.
// Typically used in combination with a blocklist to define fixed block responses or
// with a router when building a walled garden.
type StaticResolver struct {
answer []dns.RR
ns []dns.RR
extra []dns.RR
rcode int
}
var _ Resolver = &StaticResolver{}
type StaticResolverOptions struct {
// Records in zone-file format
Answer []string
NS []string
Extra []string
RCode int
}
// NewStaticResolver returns a new instance of a StaticResolver resolver.
func NewStaticResolver(opt StaticResolverOptions) (*StaticResolver, error) {
r := new(StaticResolver)
for _, record := range opt.Answer {
rr, err := dns.NewRR(record)
if err != nil {
return nil, err
}
r.answer = append(r.answer, rr)
}
for _, record := range opt.NS {
rr, err := dns.NewRR(record)
if err != nil {
return nil, err
}
r.ns = append(r.ns, rr)
}
for _, record := range opt.Extra {
rr, err := dns.NewRR(record)
if err != nil {
return nil, err
}
r.extra = append(r.extra, rr)
}
r.rcode = opt.RCode
return r, nil
}
// Resolve a DNS query by returning a fixed response.
func (r *StaticResolver) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
answer := new(dns.Msg)
answer.SetReply(q)
// Update the name of every answer record to match that of the query
answer.Answer = make([]dns.RR, 0, len(r.answer))
for _, rr := range r.answer {
r := dns.Copy(rr)
r.Header().Name = qName(q)
answer.Answer = append(answer.Answer, r)
}
answer.Ns = r.ns
answer.Extra = r.extra
answer.Rcode = r.rcode
return answer, nil
}
func (r *StaticResolver) String() string {
return fmt.Sprintf("StaticResolver")
}

37
static_test.go Normal file
View File

@@ -0,0 +1,37 @@
package rdns
import (
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
func TestStaticResolver(t *testing.T) {
opt := StaticResolverOptions{
Answer: []string{
"IN A 1.2.3.4",
},
NS: []string{
"example.com. 18000 IN A 1.2.3.4",
"example.com. 18000 IN AAAA ::1",
},
Extra: []string{
"ns1.example.com. IN A 1.1.1.1",
},
}
r, err := NewStaticResolver(opt)
require.NoError(t, err)
q := new(dns.Msg)
q.SetQuestion("test.com.", dns.TypeA)
a, err := r.Resolve(q, ClientInfo{})
require.NoError(t, err)
require.Equal(t, len(opt.Answer), len(a.Answer))
require.Equal(t, len(opt.NS), len(a.Ns))
require.Equal(t, len(opt.Extra), len(a.Extra))
require.Equal(t, "test.com.", a.Answer[0].Header().Name)
require.Equal(t, "example.com.", a.Ns[0].Header().Name)
require.Equal(t, "ns1.example.com.", a.Extra[0].Header().Name)
}