Support routing by time and weekday (#167)

* Support routing by time and weekday

* Add note about impossible routes
This commit is contained in:
Frank Olbricht
2021-06-19 13:53:08 -06:00
committed by GitHub
parent 8779d341f4
commit 8014a4d305
10 changed files with 165 additions and 25 deletions

View File

@@ -15,7 +15,7 @@ Features:
- Support for plain DNS, UDP and TCP for incoming and outgoing requests
- 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))
- 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 Client Subnet (ECS) manipulation ([RFC7871](https://tools.ietf.org/html/rfc7871))
- Support for bootstrap addresses to avoid the initial service name lookup
@@ -24,7 +24,7 @@ Features:
## 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
GO111MODULE=on go get -v github.com/folbricht/routedns/cmd/routedns

View File

@@ -128,6 +128,8 @@ type route struct {
Class string
Name string
Source string
Weekdays []string // 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'
After, Before string // Hour:Minute in 24h format, for example "14:30"
Invert bool // Invert the result of the match
Resolver string
}

View 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"

View File

@@ -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
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 {
return fmt.Errorf("failure parsing routes for router '%s' : %s", id, err.Error())
}

View File

@@ -991,7 +991,7 @@ Example config files: [response-collapse.toml](../cmd/routedns/example-config/re
### 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
@@ -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.
- `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.
- `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.
- `resolver` - The identifier of a resolver, group, or another router. Required.
@@ -1057,7 +1060,17 @@ 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)
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

View File

@@ -44,8 +44,8 @@ func Example_router() {
// Build a router that will send all "*.cloudflare.com" to the cloudflare
// resolver while everything else goes to the google resolver (default)
route1, _ := rdns.NewRoute(`\.cloudflare\.com\.$`, "", nil, "", cloudflare)
route2, _ := rdns.NewRoute("", "", nil, "", google)
route1, _ := rdns.NewRoute(`\.cloudflare\.com\.$`, "", nil, nil, "", "", "", cloudflare)
route2, _ := rdns.NewRoute("", "", nil, nil, "", "", "", google)
r := rdns.NewRouter("my-router")
r.Add(route1, route2)

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/folbricht/routedns
go 1.12
go 1.15
require (
github.com/BurntSushi/toml v0.3.1

107
route.go
View File

@@ -5,7 +5,9 @@ import (
"fmt"
"net"
"regexp"
"strconv"
"strings"
"time"
"github.com/miekg/dns"
)
@@ -15,12 +17,15 @@ type route struct {
class uint16
name *regexp.Regexp
source *net.IPNet
weekdays []time.Weekday
before *TimeOfDay
after *TimeOfDay
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) {
func NewRoute(name, class string, types, weekdays []string, before, after, source string, resolver Resolver) (*route, error) {
if resolver == nil {
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 {
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)
if err != nil {
return nil, err
@@ -47,6 +64,9 @@ func NewRoute(name, class string, types []string, source string, resolver Resolv
types: t,
class: c,
name: re,
weekdays: w,
before: b,
after: a,
source: sNet,
resolver: resolver,
}, nil
@@ -66,6 +86,30 @@ func (r *route) match(q *dns.Msg, ci ClientInfo) bool {
if r.source != nil && !r.source.Contains(ci.SourceIP) {
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
}
@@ -134,3 +178,64 @@ func stringToClass(s string) (uint16, error) {
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)
}

View File

@@ -79,7 +79,7 @@ func TestRoute(t *testing.T) {
},
}
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)
r.Invert(test.rInvert)

View File

@@ -14,8 +14,8 @@ func TestRouterType(t *testing.T) {
q := new(dns.Msg)
var ci ClientInfo
route1, _ := NewRoute("", "", []string{"MX"}, "", r1)
route2, _ := NewRoute("", "", nil, "", r2)
route1, _ := NewRoute("", "", []string{"MX"}, nil, "", "", "", r1)
route2, _ := NewRoute("", "", nil, nil, "", "", "", r2)
router := NewRouter("my-router")
router.Add(route1, route2)
@@ -41,8 +41,8 @@ func TestRouterClass(t *testing.T) {
q := new(dns.Msg)
var ci ClientInfo
route1, _ := NewRoute("", "ANY", nil, "", r1)
route2, _ := NewRoute("", "", nil, "", r2)
route1, _ := NewRoute("", "ANY", nil, nil, "", "", "", r1)
route2, _ := NewRoute("", "", nil, nil, "", "", "", r2)
router := NewRouter("my-router")
router.Add(route1, route2)
@@ -69,8 +69,8 @@ func TestRouterName(t *testing.T) {
q := new(dns.Msg)
var ci ClientInfo
route1, _ := NewRoute(`\.acme\.test\.$`, "", nil, "", r1)
route2, _ := NewRoute("", "", nil, "", r2)
route1, _ := NewRoute(`\.acme\.test\.$`, "", nil, nil, "", "", "", r1)
route2, _ := NewRoute("", "", nil, nil, "", "", "", r2)
router := NewRouter("my-router")
router.Add(route1, route2)
@@ -96,8 +96,8 @@ 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)
route1, _ := NewRoute("", "", nil, nil, "", "", "192.168.1.100/32", r1)
route2, _ := NewRoute("", "", nil, nil, "", "", "", r2)
router := NewRouter("my-router")
router.Add(route1, route2)