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

View File

@@ -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.

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 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())
} }

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)