mirror of
https://github.com/folbricht/routedns.git
synced 2026-01-05 17:20:09 -06:00
Supporting multiple types in routes and inverting of routes (#97)
This commit is contained in:
@@ -113,10 +113,12 @@ type router struct {
|
||||
}
|
||||
|
||||
type route struct {
|
||||
Type string
|
||||
Type string // Deprecated, use "Types" instead
|
||||
Types []string
|
||||
Class string
|
||||
Name string
|
||||
Source string
|
||||
Invert bool // Invert the result of the match
|
||||
Resolver string
|
||||
}
|
||||
|
||||
|
||||
33
cmd/routedns/example-config/router.toml
Normal file
33
cmd/routedns/example-config/router.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
# Simple example showing how to to use a router containing multi-type routes as well as
|
||||
# inverted routes. The first route only allows A, AAAA, MX record lookups past it. Then
|
||||
# everything under .google.com will be sent to Google DoT while everything else goes to
|
||||
# Cloudflare.
|
||||
|
||||
[listeners.local-udp]
|
||||
address = "127.0.0.1:53"
|
||||
protocol = "udp"
|
||||
resolver = "router1"
|
||||
|
||||
[listeners.local-tcp]
|
||||
address = "127.0.0.1:53"
|
||||
protocol = "tcp"
|
||||
resolver = "router1"
|
||||
|
||||
[routers.router1]
|
||||
routes = [
|
||||
{ invert = true, types = ["A", "AAAA", "MX"], resolver="static-nxdomain" }, # disallow anything that is not A, AAAA, or MX
|
||||
{ name = '(^|\.)google\.com\.$', resolver="google-dot" },
|
||||
{ resolver="cloudflare-dot" }, # default route
|
||||
]
|
||||
|
||||
[groups.static-nxdomain]
|
||||
type = "static-responder"
|
||||
rcode = 3
|
||||
|
||||
[resolvers.cloudflare-dot]
|
||||
address = "1.1.1.1:853"
|
||||
protocol = "dot"
|
||||
|
||||
[resolvers.google-dot]
|
||||
address = "8.8.8.8:853"
|
||||
protocol = "dot"
|
||||
@@ -557,9 +557,16 @@ func instantiateRouter(id string, r router, resolvers map[string]rdns.Resolver)
|
||||
if !ok {
|
||||
return fmt.Errorf("router '%s' references non-existant resolver or group '%s'", id, route.Resolver)
|
||||
}
|
||||
if err := router.Add(route.Name, route.Class, route.Type, route.Source, resolver); err != nil {
|
||||
types := route.Types
|
||||
if route.Type != "" { // Support the deprecated "Type" by just adding it to "Types" if defined
|
||||
types = append(types, route.Type)
|
||||
}
|
||||
r, err := rdns.NewRoute(route.Name, route.Class, types, route.Source, resolver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure parsing routes for router '%s' : %s", id, err.Error())
|
||||
}
|
||||
r.Invert(route.Invert)
|
||||
router.Add(r)
|
||||
}
|
||||
resolvers[id] = router
|
||||
return nil
|
||||
|
||||
@@ -938,9 +938,11 @@ Options:
|
||||
A route has the following fields:
|
||||
|
||||
- `type` - If defined, only matches queries of this type, `A`, `AAAA`, `MX`, etc. Optional.
|
||||
- `types` - List of types. If defined, only matches queries whose type is in this list. Optional.
|
||||
- `class` - If defined, only matches queries of this class (`IN`, `CH`, `HS`, `NONE`, `ANY`). Optional.
|
||||
- `name` - A regular expression that is applied to the query name. Note that dots in domain names need to be escaped. Optional.
|
||||
- `source` - Network in CIDR notation. Used to route based on client IP. Optional.
|
||||
- `invert` - Invert the result of the matching if set to `true`. Optional.
|
||||
- `resolver` - The identifier of a resolver, group, or another router. Required.
|
||||
|
||||
Examples:
|
||||
@@ -955,6 +957,16 @@ routes = [
|
||||
]
|
||||
```
|
||||
|
||||
Send all queries for A, AAAA, and MX records under `google.com` to a non-default resolver. Note the plural in `types` which expects a list.
|
||||
|
||||
```toml
|
||||
[routers.router1]
|
||||
routes = [
|
||||
{ name = '(^|\.)google\.com\.$', types = ["A", "AAAA", "MX"], resolver="google-udp" },
|
||||
{ resolver="cloudflare-dot" }, # default route
|
||||
]
|
||||
```
|
||||
|
||||
Route queries from a specific IP to a different resolver.
|
||||
|
||||
```toml
|
||||
@@ -965,7 +977,21 @@ routes = [
|
||||
]
|
||||
```
|
||||
|
||||
Example config files: [split-dns.toml](../cmd/routedns/example-config/split-dns.toml), [block-split-cache.toml](../cmd/routedns/example-config/block-split-cache.toml), [family-browsing.toml](../cmd/routedns/example-config/family-browsing.toml),[walled-garden.toml](../cmd/routedns/example-config/walled-garden.toml)
|
||||
Disallow all queries for records that are not of type A, AAAA, or MX by responding with NXDOMAIN.
|
||||
|
||||
```toml
|
||||
[routers.router1]
|
||||
routes = [
|
||||
{ invert = true, types = ["A", "AAAA", "MX"], resolver="static-nxdomain" },
|
||||
{ resolver="cloudflare-dot" },
|
||||
]
|
||||
|
||||
[groups.static-nxdomain]
|
||||
type = "static-responder"
|
||||
rcode = 3
|
||||
```
|
||||
|
||||
Example config files: [split-dns.toml](../cmd/routedns/example-config/split-dns.toml), [block-split-cache.toml](../cmd/routedns/example-config/block-split-cache.toml), [family-browsing.toml](../cmd/routedns/example-config/family-browsing.toml), [walled-garden.toml](../cmd/routedns/example-config/walled-garden.toml), [router.toml](../cmd/routedns/example-config/router.toml)
|
||||
|
||||
### Rate Limiter
|
||||
|
||||
|
||||
@@ -43,10 +43,11 @@ func Example_router() {
|
||||
cloudflare, _ := rdns.NewDNSClient("cf-dns", "1.1.1.1:53", "udp", rdns.DNSClientOptions{})
|
||||
|
||||
// Build a router that will send all "*.cloudflare.com" to the cloudflare
|
||||
// resolvber while everything else goes to the google resolver (default)
|
||||
// resolver while everything else goes to the google resolver (default)
|
||||
route1, _ := rdns.NewRoute(`\.cloudflare\.com\.$`, "", nil, "", cloudflare)
|
||||
route2, _ := rdns.NewRoute("", "", nil, "", google)
|
||||
r := rdns.NewRouter("my-router")
|
||||
_ = r.Add(`\.cloudflare\.com\.$`, "", "", "", cloudflare)
|
||||
_ = r.Add("", "", "", "", google)
|
||||
r.Add(route1, route2)
|
||||
|
||||
// Build a query
|
||||
q := new(dns.Msg)
|
||||
|
||||
136
route.go
Normal file
136
route.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package rdns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type route struct {
|
||||
types []uint16
|
||||
class uint16
|
||||
name *regexp.Regexp
|
||||
source *net.IPNet
|
||||
inverted bool // invert the matching behavior
|
||||
resolver Resolver
|
||||
}
|
||||
|
||||
// NewRoute initializes a route from string parameters.
|
||||
func NewRoute(name, class string, types []string, source string, resolver Resolver) (*route, error) {
|
||||
if resolver == nil {
|
||||
return nil, errors.New("no resolver defined for route")
|
||||
}
|
||||
t, err := stringToType(types)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := stringToClass(class)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
re, err := regexp.Compile(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sNet *net.IPNet
|
||||
if source != "" {
|
||||
_, sNet, err = net.ParseCIDR(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &route{
|
||||
types: t,
|
||||
class: c,
|
||||
name: re,
|
||||
source: sNet,
|
||||
resolver: resolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *route) match(q *dns.Msg, ci ClientInfo) bool {
|
||||
question := q.Question[0]
|
||||
if !r.matchType(question.Qtype) {
|
||||
return r.inverted
|
||||
}
|
||||
if r.class != 0 && r.class != question.Qclass {
|
||||
return r.inverted
|
||||
}
|
||||
if !r.name.MatchString(question.Name) {
|
||||
return r.inverted
|
||||
}
|
||||
if r.source != nil && !r.source.Contains(ci.SourceIP) {
|
||||
return r.inverted
|
||||
}
|
||||
return !r.inverted
|
||||
}
|
||||
|
||||
func (r *route) Invert(value bool) {
|
||||
r.inverted = value
|
||||
}
|
||||
|
||||
func (r *route) String() string {
|
||||
if r.isDefault() {
|
||||
return fmt.Sprintf("default->%s", r.resolver)
|
||||
}
|
||||
return fmt.Sprintf("%s->%s", r.name, r.resolver)
|
||||
}
|
||||
|
||||
func (r *route) isDefault() bool {
|
||||
return r.class == 0 && len(r.types) == 0 && r.name.String() == ""
|
||||
}
|
||||
|
||||
func (r *route) matchType(typ uint16) bool {
|
||||
if len(r.types) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, t := range r.types {
|
||||
if t == typ {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Convert DNS type strings into the numerical type, for example "A" -> 1.
|
||||
func stringToType(s []string) ([]uint16, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var types []uint16
|
||||
loop:
|
||||
for _, typ := range s {
|
||||
for k, v := range dns.TypeToString {
|
||||
if v == strings.ToUpper(typ) {
|
||||
types = append(types, k)
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unknown type '%s'", s)
|
||||
}
|
||||
return types, nil
|
||||
}
|
||||
|
||||
// Convert a DNS class string into is numerical form, for example "INET" -> 1.
|
||||
func stringToClass(s string) (uint16, error) {
|
||||
switch strings.ToUpper(s) {
|
||||
case "":
|
||||
return 0, nil
|
||||
case "IN", "INET":
|
||||
return 1, nil
|
||||
case "CH":
|
||||
return 3, nil
|
||||
case "HS":
|
||||
return 4, nil
|
||||
case "NONE":
|
||||
return 254, nil
|
||||
case "ANY":
|
||||
return 255, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown class '%s'", s)
|
||||
}
|
||||
}
|
||||
93
route_test.go
Normal file
93
route_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package rdns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRoute(t *testing.T) {
|
||||
tests := []struct {
|
||||
rName string
|
||||
rType []string
|
||||
rClass string
|
||||
rInvert bool
|
||||
qName string
|
||||
qType uint16
|
||||
qClass uint16
|
||||
match bool
|
||||
}{
|
||||
{
|
||||
rName: "\\.google\\.com$",
|
||||
qType: dns.TypeA,
|
||||
qName: "bla.google.com",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
rName: "\\.google\\.com$",
|
||||
qType: dns.TypeA,
|
||||
qName: "google.com",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
rType: []string{"MX"},
|
||||
rName: "google\\.com$",
|
||||
qType: dns.TypeA,
|
||||
qName: "google.com",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
rType: []string{"MX", "A"},
|
||||
rName: "google\\.com$",
|
||||
qType: dns.TypeA,
|
||||
qName: "google.com",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
rType: []string{"MX"},
|
||||
rName: "google\\.com$",
|
||||
qType: dns.TypeMX,
|
||||
qName: "google.com",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
rType: []string{"MX"},
|
||||
rName: "google\\.com$",
|
||||
rInvert: true,
|
||||
qType: dns.TypeMX,
|
||||
qName: "google.com",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
rClass: "INET",
|
||||
rType: []string{"A"},
|
||||
rName: "google\\.com$",
|
||||
qClass: dns.ClassANY,
|
||||
qType: dns.TypeA,
|
||||
qName: "google.com",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
rClass: "INET",
|
||||
rType: []string{"A"},
|
||||
rName: "google\\.com$",
|
||||
qClass: dns.ClassINET,
|
||||
qType: dns.TypeA,
|
||||
qName: "google.com",
|
||||
match: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
r, err := NewRoute(test.rName, test.rClass, test.rType, "", &TestResolver{})
|
||||
require.NoError(t, err)
|
||||
r.Invert(test.rInvert)
|
||||
|
||||
q := new(dns.Msg)
|
||||
q.Question = make([]dns.Question, 1)
|
||||
q.Question[0] = dns.Question{Name: test.qName, Qtype: test.qType, Qclass: test.qClass}
|
||||
|
||||
match := r.match(q, ClientInfo{})
|
||||
require.Equal(t, test.match, match)
|
||||
}
|
||||
}
|
||||
97
router.go
97
router.go
@@ -4,8 +4,6 @@ import (
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -55,16 +53,7 @@ func (r *Router) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
|
||||
question := q.Question[0]
|
||||
log := logger(r.id, q, ci)
|
||||
for _, route := range r.routes {
|
||||
if route.typ != 0 && route.typ != question.Qtype {
|
||||
continue
|
||||
}
|
||||
if route.class != 0 && route.class != question.Qclass {
|
||||
continue
|
||||
}
|
||||
if !route.name.MatchString(question.Name) {
|
||||
continue
|
||||
}
|
||||
if route.source != nil && !route.source.Contains(ci.SourceIP) {
|
||||
if !route.match(q, ci) {
|
||||
continue
|
||||
}
|
||||
log.WithField("resolver", route.resolver.String()).Debug("routing query to resolver")
|
||||
@@ -84,91 +73,11 @@ func (r *Router) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
|
||||
// routes won't have any impact. Name is a regular expression that is
|
||||
// applied to the name in the first question section of the DNS message.
|
||||
// Source is an IP or network in CIDR format.
|
||||
func (r *Router) Add(name, class, typ, source string, resolver Resolver) error {
|
||||
t, err := stringToType(typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := stringToClass(class)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
re, err := regexp.Compile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sNet *net.IPNet
|
||||
if source != "" {
|
||||
_, sNet, err = net.ParseCIDR(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
newRoute := &route{
|
||||
typ: t,
|
||||
class: c,
|
||||
name: re,
|
||||
source: sNet,
|
||||
resolver: resolver,
|
||||
}
|
||||
|
||||
r.routes = append(r.routes, newRoute)
|
||||
func (r *Router) Add(routes ...*route) {
|
||||
r.routes = append(r.routes, routes...)
|
||||
r.metrics.available.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) String() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
// Convert DNS type strings into the numberical type, for example "A" -> 1.
|
||||
func stringToType(s string) (uint16, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
for k, v := range dns.TypeToString {
|
||||
if v == s {
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("unknown type '%s'", s)
|
||||
}
|
||||
|
||||
// Convert a DNS class string into is numerical form, for example "INET" -> 1.
|
||||
func stringToClass(s string) (uint16, error) {
|
||||
switch s {
|
||||
case "":
|
||||
return 0, nil
|
||||
case "IN":
|
||||
return 1, nil
|
||||
case "CH":
|
||||
return 3, nil
|
||||
case "HS":
|
||||
return 4, nil
|
||||
case "NONE":
|
||||
return 254, nil
|
||||
case "ANY":
|
||||
return 255, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown class '%s'", s)
|
||||
}
|
||||
}
|
||||
|
||||
type route struct {
|
||||
typ uint16
|
||||
class uint16
|
||||
name *regexp.Regexp
|
||||
source *net.IPNet
|
||||
resolver Resolver
|
||||
}
|
||||
|
||||
func (r route) String() string {
|
||||
if r.isDefault() {
|
||||
return fmt.Sprintf("default->%s", r.resolver)
|
||||
}
|
||||
return fmt.Sprintf("%s:%s->%s", r.name, dns.Type(r.typ), r.resolver)
|
||||
}
|
||||
|
||||
func (r route) isDefault() bool {
|
||||
return r.typ == 0 && r.name.String() == ""
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ func TestRouterType(t *testing.T) {
|
||||
q := new(dns.Msg)
|
||||
var ci ClientInfo
|
||||
|
||||
route1, _ := NewRoute("", "", []string{"MX"}, "", r1)
|
||||
route2, _ := NewRoute("", "", nil, "", r2)
|
||||
|
||||
router := NewRouter("my-router")
|
||||
_ = router.Add("", "", "MX", "", r1)
|
||||
_ = router.Add("", "", "", "", r2)
|
||||
router.Add(route1, route2)
|
||||
|
||||
// Not MX record, should go to r2
|
||||
q.SetQuestion("acme.test.", dns.TypeA)
|
||||
@@ -39,9 +41,11 @@ func TestRouterClass(t *testing.T) {
|
||||
q := new(dns.Msg)
|
||||
var ci ClientInfo
|
||||
|
||||
route1, _ := NewRoute("", "ANY", nil, "", r1)
|
||||
route2, _ := NewRoute("", "", nil, "", r2)
|
||||
|
||||
router := NewRouter("my-router")
|
||||
_ = router.Add("", "ANY", "", "", r1)
|
||||
_ = router.Add("", "", "", "", r2)
|
||||
router.Add(route1, route2)
|
||||
|
||||
// ClassINET question, should go to r2
|
||||
q.SetQuestion("acme.test.", dns.TypeA)
|
||||
@@ -50,7 +54,7 @@ func TestRouterClass(t *testing.T) {
|
||||
require.Equal(t, 0, r1.HitCount())
|
||||
require.Equal(t, 1, r2.HitCount())
|
||||
|
||||
// ClassAny shuold go to r1
|
||||
// ClassAny should go to r1
|
||||
q.Question = make([]dns.Question, 1)
|
||||
q.Question[0] = dns.Question{"miek.nl.", dns.TypeMX, dns.ClassANY}
|
||||
_, err = router.Resolve(q, ci)
|
||||
@@ -65,9 +69,11 @@ func TestRouterName(t *testing.T) {
|
||||
q := new(dns.Msg)
|
||||
var ci ClientInfo
|
||||
|
||||
route1, _ := NewRoute(`\.acme\.test\.$`, "", nil, "", r1)
|
||||
route2, _ := NewRoute("", "", nil, "", r2)
|
||||
|
||||
router := NewRouter("my-router")
|
||||
_ = router.Add(`\.acme\.test\.$`, "", "", "", r1)
|
||||
_ = router.Add("", "", "", "", r2)
|
||||
router.Add(route1, route2)
|
||||
|
||||
// No match, should go to r2
|
||||
q.SetQuestion("bla.test.", dns.TypeA)
|
||||
@@ -90,9 +96,11 @@ func TestRouterSource(t *testing.T) {
|
||||
q := new(dns.Msg)
|
||||
q.SetQuestion("acme.test.", dns.TypeA)
|
||||
|
||||
route1, _ := NewRoute("", "", nil, "192.168.1.100/32", r1)
|
||||
route2, _ := NewRoute("", "", nil, "", r2)
|
||||
|
||||
router := NewRouter("my-router")
|
||||
_ = router.Add("", "", "", "192.168.1.100/32", r1)
|
||||
_ = router.Add("", "", "", "", r2)
|
||||
router.Add(route1, route2)
|
||||
|
||||
// No match, should go to r2
|
||||
_, err := router.Resolve(q, ClientInfo{SourceIP: net.ParseIP("192.168.1.50")})
|
||||
|
||||
Reference in New Issue
Block a user