mirror of
https://github.com/folbricht/routedns.git
synced 2025-12-20 09:00:01 -06:00
Support routing by time and weekday (#167)
* Support routing by time and weekday * Add note about impossible routes
This commit is contained in:
@@ -15,7 +15,7 @@ Features:
|
|||||||
- Support for plain DNS, UDP and TCP for incoming and outgoing requests
|
- Support for plain DNS, UDP and TCP for incoming and outgoing requests
|
||||||
- Connection reuse and pipelining queries for efficiency
|
- Connection reuse and pipelining queries for efficiency
|
||||||
- Multiple failover and load-balancing algorithms, caching, in-line query/response modification and translation (full list [here](doc/configuration.md))
|
- Multiple failover and load-balancing algorithms, caching, in-line query/response modification and translation (full list [here](doc/configuration.md))
|
||||||
- Routing of queries based on query type, query name, or client IP
|
- Routing of queries based on query type, class, query name, time, or client IP
|
||||||
- EDNS0 query and response padding ([RFC7830](https://tools.ietf.org/html/rfc7830), [RFC8467](https://tools.ietf.org/html/rfc8467))
|
- EDNS0 query and response padding ([RFC7830](https://tools.ietf.org/html/rfc7830), [RFC8467](https://tools.ietf.org/html/rfc8467))
|
||||||
- EDNS0 Client Subnet (ECS) manipulation ([RFC7871](https://tools.ietf.org/html/rfc7871))
|
- EDNS0 Client Subnet (ECS) manipulation ([RFC7871](https://tools.ietf.org/html/rfc7871))
|
||||||
- Support for bootstrap addresses to avoid the initial service name lookup
|
- Support for bootstrap addresses to avoid the initial service name lookup
|
||||||
@@ -24,7 +24,7 @@ Features:
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install [Go](https://golang.org/dl) version 1.13+ then run the following to build the binary. It'll be placed in $HOME/go/bin by default:
|
Install [Go](https://golang.org/dl) version 1.15+ then run the following to build the binary. It'll be placed in $HOME/go/bin by default:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
GO111MODULE=on go get -v github.com/folbricht/routedns/cmd/routedns
|
GO111MODULE=on go get -v github.com/folbricht/routedns/cmd/routedns
|
||||||
|
|||||||
@@ -123,13 +123,15 @@ type router struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type route struct {
|
type route struct {
|
||||||
Type string // Deprecated, use "Types" instead
|
Type string // Deprecated, use "Types" instead
|
||||||
Types []string
|
Types []string
|
||||||
Class string
|
Class string
|
||||||
Name string
|
Name string
|
||||||
Source string
|
Source string
|
||||||
Invert bool // Invert the result of the match
|
Weekdays []string // 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'
|
||||||
Resolver string
|
After, Before string // Hour:Minute in 24h format, for example "14:30"
|
||||||
|
Invert bool // Invert the result of the match
|
||||||
|
Resolver string
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig reads a config file and returns the decoded structure.
|
// LoadConfig reads a config file and returns the decoded structure.
|
||||||
|
|||||||
20
cmd/routedns/example-config/router-time.toml
Normal file
20
cmd/routedns/example-config/router-time.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Routing traffic based on time of day and a weekday.
|
||||||
|
|
||||||
|
[listeners.local-udp]
|
||||||
|
address = "127.0.0.1:53"
|
||||||
|
protocol = "udp"
|
||||||
|
resolver = "router1"
|
||||||
|
|
||||||
|
[routers.router1]
|
||||||
|
routes = [
|
||||||
|
{ name = '(^|\.)twitter\.com\.$', weekdays = ["sat", "sun"], after = "09:00", before = "17:00", resolver="static-nxdomain" }, # No Twitter on weekends from 9am-5pm!
|
||||||
|
{ resolver="cloudflare-dot" }, # default route
|
||||||
|
]
|
||||||
|
|
||||||
|
[groups.static-nxdomain]
|
||||||
|
type = "static-responder"
|
||||||
|
rcode = 3
|
||||||
|
|
||||||
|
[resolvers.cloudflare-dot]
|
||||||
|
address = "1.1.1.1:853"
|
||||||
|
protocol = "dot"
|
||||||
@@ -603,7 +603,7 @@ func instantiateRouter(id string, r router, resolvers map[string]rdns.Resolver)
|
|||||||
if route.Type != "" { // Support the deprecated "Type" by just adding it to "Types" if defined
|
if route.Type != "" { // Support the deprecated "Type" by just adding it to "Types" if defined
|
||||||
types = append(types, route.Type)
|
types = append(types, route.Type)
|
||||||
}
|
}
|
||||||
r, err := rdns.NewRoute(route.Name, route.Class, types, route.Source, resolver)
|
r, err := rdns.NewRoute(route.Name, route.Class, types, route.Weekdays, route.Before, route.After, route.Source, resolver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failure parsing routes for router '%s' : %s", id, err.Error())
|
return fmt.Errorf("failure parsing routes for router '%s' : %s", id, err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -991,7 +991,7 @@ Example config files: [response-collapse.toml](../cmd/routedns/example-config/re
|
|||||||
|
|
||||||
### Router
|
### Router
|
||||||
|
|
||||||
Routers are used to direct queries to specific upstream resolvers, modifier, or to other routers based on the query type, name, or client information. Each router contains at least one route. Routes are are evaluated in the order they are defined and the first match will be used. Routes that match on the query name are regular expressions. Typically the last route should not have a class, type or name, making it the default route.
|
Routers are used to direct queries to specific upstream resolvers, modifiers, or to other routers based on the query type, name, time of day, or client information. Each router contains at least one route. Routes are are evaluated in the order they are defined and the first match will be used. Routes that match on the query name are regular expressions. Typically the last route should not have a class, type or name, making it the default route.
|
||||||
|
|
||||||
#### Configuration
|
#### Configuration
|
||||||
|
|
||||||
@@ -1008,6 +1008,9 @@ A route has the following fields:
|
|||||||
- `class` - If defined, only matches queries of this class (`IN`, `CH`, `HS`, `NONE`, `ANY`). 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.
|
- `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.
|
- `source` - Network in CIDR notation. Used to route based on client IP. Optional.
|
||||||
|
- `weekdays` - List of weekdays this route should match on. Possible values: `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`. Uses local time, not UTC.
|
||||||
|
- `after` - Time of day in the format HH:mm after which the rule matches. Uses 24h format. For example `09:00`. Note that together with the `before` parameter it is possible to accidentally write routes that can never trigger. For example `after=12:00 before=11:00` can never match as both conditions have to be met for the route to be used.
|
||||||
|
- `before` - Time of day in the format HH:mm before which the rule matches. Uses 24h format. For example `17:30`.
|
||||||
- `invert` - Invert the result of the matching if set to `true`. Optional.
|
- `invert` - Invert the result of the matching if set to `true`. Optional.
|
||||||
- `resolver` - The identifier of a resolver, group, or another router. Required.
|
- `resolver` - The identifier of a resolver, group, or another router. Required.
|
||||||
|
|
||||||
@@ -1057,7 +1060,17 @@ type = "static-responder"
|
|||||||
rcode = 3
|
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)
|
Use a different upstream resolver on weekends between 9am and 5pm.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[routers.router1]
|
||||||
|
routes = [
|
||||||
|
{ weekdays = ["sat", "sun"], after = "09:00", before = "17:00", resolver="google-dot" },
|
||||||
|
{ resolver="cloudflare-dot" },
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
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), [router-time.toml](../cmd/routedns/example-config/router-time.toml)
|
||||||
|
|
||||||
### Rate Limiter
|
### Rate Limiter
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ func Example_router() {
|
|||||||
|
|
||||||
// Build a router that will send all "*.cloudflare.com" to the cloudflare
|
// Build a router that will send all "*.cloudflare.com" to the cloudflare
|
||||||
// resolver 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)
|
route1, _ := rdns.NewRoute(`\.cloudflare\.com\.$`, "", nil, nil, "", "", "", cloudflare)
|
||||||
route2, _ := rdns.NewRoute("", "", nil, "", google)
|
route2, _ := rdns.NewRoute("", "", nil, nil, "", "", "", google)
|
||||||
r := rdns.NewRouter("my-router")
|
r := rdns.NewRouter("my-router")
|
||||||
r.Add(route1, route2)
|
r.Add(route1, route2)
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/folbricht/routedns
|
module github.com/folbricht/routedns
|
||||||
|
|
||||||
go 1.12
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v0.3.1
|
github.com/BurntSushi/toml v0.3.1
|
||||||
|
|||||||
107
route.go
107
route.go
@@ -5,7 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -15,12 +17,15 @@ type route struct {
|
|||||||
class uint16
|
class uint16
|
||||||
name *regexp.Regexp
|
name *regexp.Regexp
|
||||||
source *net.IPNet
|
source *net.IPNet
|
||||||
|
weekdays []time.Weekday
|
||||||
|
before *TimeOfDay
|
||||||
|
after *TimeOfDay
|
||||||
inverted bool // invert the matching behavior
|
inverted bool // invert the matching behavior
|
||||||
resolver Resolver
|
resolver Resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRoute initializes a route from string parameters.
|
// NewRoute initializes a route from string parameters.
|
||||||
func NewRoute(name, class string, types []string, source string, resolver Resolver) (*route, error) {
|
func NewRoute(name, class string, types, weekdays []string, before, after, source string, resolver Resolver) (*route, error) {
|
||||||
if resolver == nil {
|
if resolver == nil {
|
||||||
return nil, errors.New("no resolver defined for route")
|
return nil, errors.New("no resolver defined for route")
|
||||||
}
|
}
|
||||||
@@ -28,6 +33,18 @@ func NewRoute(name, class string, types []string, source string, resolver Resolv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
w, err := stringsToWeekdays(weekdays)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b, err := parseTimeOfDay(before)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a, err := parseTimeOfDay(after)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
c, err := stringToClass(class)
|
c, err := stringToClass(class)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -47,6 +64,9 @@ func NewRoute(name, class string, types []string, source string, resolver Resolv
|
|||||||
types: t,
|
types: t,
|
||||||
class: c,
|
class: c,
|
||||||
name: re,
|
name: re,
|
||||||
|
weekdays: w,
|
||||||
|
before: b,
|
||||||
|
after: a,
|
||||||
source: sNet,
|
source: sNet,
|
||||||
resolver: resolver,
|
resolver: resolver,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -66,6 +86,30 @@ func (r *route) match(q *dns.Msg, ci ClientInfo) bool {
|
|||||||
if r.source != nil && !r.source.Contains(ci.SourceIP) {
|
if r.source != nil && !r.source.Contains(ci.SourceIP) {
|
||||||
return r.inverted
|
return r.inverted
|
||||||
}
|
}
|
||||||
|
if len(r.weekdays) > 0 || r.before != nil || r.after != nil {
|
||||||
|
now := time.Now().Local()
|
||||||
|
hour := now.Hour()
|
||||||
|
minute := now.Minute()
|
||||||
|
if len(r.weekdays) > 0 {
|
||||||
|
weekday := now.Weekday()
|
||||||
|
var weekdayMatch bool
|
||||||
|
for _, wd := range r.weekdays {
|
||||||
|
if weekday == wd {
|
||||||
|
weekdayMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !weekdayMatch {
|
||||||
|
return r.inverted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.before != nil && !r.before.isAfter(hour, minute) {
|
||||||
|
return r.inverted
|
||||||
|
}
|
||||||
|
if r.after != nil && !r.after.isBefore(hour, minute) {
|
||||||
|
return r.inverted
|
||||||
|
}
|
||||||
|
}
|
||||||
return !r.inverted
|
return !r.inverted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,3 +178,64 @@ func stringToClass(s string) (uint16, error) {
|
|||||||
return 0, fmt.Errorf("unknown class '%s'", s)
|
return 0, fmt.Errorf("unknown class '%s'", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stringsToWeekdays(weekdays []string) ([]time.Weekday, error) {
|
||||||
|
var result []time.Weekday
|
||||||
|
for _, day := range weekdays {
|
||||||
|
var weekday time.Weekday
|
||||||
|
switch day {
|
||||||
|
case "mon":
|
||||||
|
weekday = time.Monday
|
||||||
|
case "tue":
|
||||||
|
weekday = time.Tuesday
|
||||||
|
case "wed":
|
||||||
|
weekday = time.Wednesday
|
||||||
|
case "thu":
|
||||||
|
weekday = time.Thursday
|
||||||
|
case "fri":
|
||||||
|
weekday = time.Friday
|
||||||
|
case "sat":
|
||||||
|
weekday = time.Saturday
|
||||||
|
case "sun":
|
||||||
|
weekday = time.Sunday
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unrecognized weekday %q, must be 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'", day)
|
||||||
|
}
|
||||||
|
result = append(result, weekday)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeOfDay struct {
|
||||||
|
hour, minute int
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTimeOfDay(t string) (*TimeOfDay, error) {
|
||||||
|
if t == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
f := strings.SplitN(t, ":", 2)
|
||||||
|
hour, err := strconv.Atoi(f[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var min int
|
||||||
|
if len(f) > 1 {
|
||||||
|
min, err = strconv.Atoi(f[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &TimeOfDay{
|
||||||
|
hour: hour,
|
||||||
|
minute: min,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TimeOfDay) isBefore(hour, minute int) bool {
|
||||||
|
return t.hour < hour || (t.hour == hour && t.minute <= minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TimeOfDay) isAfter(hour, minute int) bool {
|
||||||
|
return t.hour > hour || (t.hour == hour && t.minute > minute)
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func TestRoute(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
r, err := NewRoute(test.rName, test.rClass, test.rType, "", &TestResolver{})
|
r, err := NewRoute(test.rName, test.rClass, test.rType, nil, "", "", "", &TestResolver{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
r.Invert(test.rInvert)
|
r.Invert(test.rInvert)
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ func TestRouterType(t *testing.T) {
|
|||||||
q := new(dns.Msg)
|
q := new(dns.Msg)
|
||||||
var ci ClientInfo
|
var ci ClientInfo
|
||||||
|
|
||||||
route1, _ := NewRoute("", "", []string{"MX"}, "", r1)
|
route1, _ := NewRoute("", "", []string{"MX"}, nil, "", "", "", r1)
|
||||||
route2, _ := NewRoute("", "", nil, "", r2)
|
route2, _ := NewRoute("", "", nil, nil, "", "", "", r2)
|
||||||
|
|
||||||
router := NewRouter("my-router")
|
router := NewRouter("my-router")
|
||||||
router.Add(route1, route2)
|
router.Add(route1, route2)
|
||||||
@@ -41,8 +41,8 @@ func TestRouterClass(t *testing.T) {
|
|||||||
q := new(dns.Msg)
|
q := new(dns.Msg)
|
||||||
var ci ClientInfo
|
var ci ClientInfo
|
||||||
|
|
||||||
route1, _ := NewRoute("", "ANY", nil, "", r1)
|
route1, _ := NewRoute("", "ANY", nil, nil, "", "", "", r1)
|
||||||
route2, _ := NewRoute("", "", nil, "", r2)
|
route2, _ := NewRoute("", "", nil, nil, "", "", "", r2)
|
||||||
|
|
||||||
router := NewRouter("my-router")
|
router := NewRouter("my-router")
|
||||||
router.Add(route1, route2)
|
router.Add(route1, route2)
|
||||||
@@ -69,8 +69,8 @@ func TestRouterName(t *testing.T) {
|
|||||||
q := new(dns.Msg)
|
q := new(dns.Msg)
|
||||||
var ci ClientInfo
|
var ci ClientInfo
|
||||||
|
|
||||||
route1, _ := NewRoute(`\.acme\.test\.$`, "", nil, "", r1)
|
route1, _ := NewRoute(`\.acme\.test\.$`, "", nil, nil, "", "", "", r1)
|
||||||
route2, _ := NewRoute("", "", nil, "", r2)
|
route2, _ := NewRoute("", "", nil, nil, "", "", "", r2)
|
||||||
|
|
||||||
router := NewRouter("my-router")
|
router := NewRouter("my-router")
|
||||||
router.Add(route1, route2)
|
router.Add(route1, route2)
|
||||||
@@ -96,8 +96,8 @@ func TestRouterSource(t *testing.T) {
|
|||||||
q := new(dns.Msg)
|
q := new(dns.Msg)
|
||||||
q.SetQuestion("acme.test.", dns.TypeA)
|
q.SetQuestion("acme.test.", dns.TypeA)
|
||||||
|
|
||||||
route1, _ := NewRoute("", "", nil, "192.168.1.100/32", r1)
|
route1, _ := NewRoute("", "", nil, nil, "", "", "192.168.1.100/32", r1)
|
||||||
route2, _ := NewRoute("", "", nil, "", r2)
|
route2, _ := NewRoute("", "", nil, nil, "", "", "", r2)
|
||||||
|
|
||||||
router := NewRouter("my-router")
|
router := NewRouter("my-router")
|
||||||
router.Add(route1, route2)
|
router.Add(route1, route2)
|
||||||
|
|||||||
Reference in New Issue
Block a user