mirror of
https://github.com/folbricht/routedns.git
synced 2026-01-04 16:40:44 -06:00
Add static-responder group (#25)
* Add static-responder group * Missing files * Fix example file
This commit is contained in:
31
README.md
31
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
53
cmd/routedns/example-config/walled-garden.toml
Normal file
53
cmd/routedns/example-config/walled-garden.toml
Normal 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"
|
||||
@@ -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
80
static.go
Normal 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
37
static_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user