blocky/resolver/blocking_resolver.go

634 lines
16 KiB
Go
Raw Normal View History

2020-01-12 18:23:35 +01:00
package resolver
import (
"context"
2020-01-12 18:23:35 +01:00
"fmt"
"net"
"slices"
2020-01-12 18:23:35 +01:00
"sort"
"strings"
"sync"
"sync/atomic"
2020-04-08 23:03:07 +02:00
"time"
2020-01-12 18:23:35 +01:00
"github.com/0xERR0R/blocky/cache/expirationcache"
"golang.org/x/exp/maps"
"github.com/hashicorp/go-multierror"
2021-08-25 22:06:34 +02:00
"github.com/0xERR0R/blocky/api"
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/evt"
"github.com/0xERR0R/blocky/lists"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/redis"
2021-08-25 22:06:34 +02:00
"github.com/0xERR0R/blocky/util"
2021-02-22 22:29:34 +01:00
2020-01-12 18:23:35 +01:00
"github.com/miekg/dns"
2021-02-22 22:29:34 +01:00
"github.com/sirupsen/logrus"
2020-01-12 18:23:35 +01:00
)
const defaultBlockingCleanUpInterval = 5 * time.Second
2023-11-21 22:27:51 +01:00
func createBlockHandler(cfg config.Blocking) (blockHandler, error) {
cfgBlockType := cfg.BlockType
2020-01-12 18:23:35 +01:00
if strings.EqualFold(cfgBlockType, "NXDOMAIN") {
return nxDomainBlockHandler{}, nil
2020-01-12 18:23:35 +01:00
}
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
blockTime := cfg.BlockTTL.SecondsU32()
if strings.EqualFold(cfgBlockType, "ZEROIP") {
return zeroIPBlockHandler{
BlockTimeSec: blockTime,
}, nil
}
var ips []net.IP
2020-01-12 18:23:35 +01:00
for _, part := range strings.Split(cfgBlockType, ",") {
if ip := net.ParseIP(strings.TrimSpace(part)); ip != nil {
ips = append(ips, ip)
}
}
if len(ips) > 0 {
return ipBlockHandler{
destinations: ips,
BlockTimeSec: blockTime,
fallbackHandler: zeroIPBlockHandler{
BlockTimeSec: blockTime,
},
}, nil
}
return nil,
fmt.Errorf("unknown blockType '%s', please use one of: ZeroIP, NxDomain or specify destination IP address(es)",
cfgBlockType)
2020-01-12 18:23:35 +01:00
}
2020-04-08 23:03:07 +02:00
type status struct {
// true: blocking of all groups is enabled
// false: blocking is disabled. Either all groups or only particular
enabled bool
disabledGroups []string
enableTimer *time.Timer
disableEnd time.Time
lock sync.RWMutex
2020-04-08 23:03:07 +02:00
}
// BlockingResolver checks request's question (domain name) against allow/denylists
2020-01-12 18:23:35 +01:00
type BlockingResolver struct {
2023-11-21 22:27:51 +01:00
configurable[*config.Blocking]
2020-01-12 18:23:35 +01:00
NextResolver
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
typed
denylistMatcher *lists.ListCache
allowlistMatcher *lists.ListCache
blockHandler blockHandler
allowlistOnlyGroups map[string]bool
2021-01-19 21:52:24 +01:00
status *status
clientGroupsBlock map[string][]string
redisClient *redis.Client
fqdnIPCache expirationcache.ExpiringCache[[]net.IP]
2020-01-12 18:23:35 +01:00
}
2023-11-21 22:27:51 +01:00
func clientGroupsBlock(cfg config.Blocking) map[string][]string {
cgb := make(map[string][]string, len(cfg.ClientGroupsBlock))
for identifier, cfgGroups := range cfg.ClientGroupsBlock {
for _, ipart := range strings.Split(strings.ToLower(identifier), ",") {
existingGroups, found := cgb[ipart]
if found {
cgb[ipart] = append(existingGroups, cfgGroups...)
} else {
cgb[ipart] = cfgGroups
}
}
}
return cgb
}
2021-02-26 21:44:53 +01:00
// NewBlockingResolver returns a new configured instance of the resolver
func NewBlockingResolver(ctx context.Context,
2023-11-21 22:27:51 +01:00
cfg config.Blocking,
redis *redis.Client,
bootstrap *Bootstrap,
) (r *BlockingResolver, err error) {
blockHandler, err := createBlockHandler(cfg)
if err != nil {
return nil, err
}
2023-04-17 18:21:56 +02:00
downloader := lists.NewDownloader(cfg.Loading.Downloads, bootstrap.NewHTTPTransport())
denylistMatcher, blErr := lists.NewListCache(ctx, lists.ListCacheTypeDenylist,
cfg.Loading, cfg.Denylists, downloader)
allowlistMatcher, wlErr := lists.NewListCache(ctx, lists.ListCacheTypeAllowlist,
cfg.Loading, cfg.Allowlists, downloader)
allowlistOnlyGroups := determineAllowlistOnlyGroups(&cfg)
2020-01-12 18:23:35 +01:00
err = multierror.Append(err, blErr, wlErr).ErrorOrNil()
2023-04-17 18:21:56 +02:00
if err != nil {
return nil, err
}
2020-04-08 23:03:07 +02:00
res := &BlockingResolver{
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
configurable: withConfig(&cfg),
typed: withType("blocking"),
blockHandler: blockHandler,
denylistMatcher: denylistMatcher,
allowlistMatcher: allowlistMatcher,
allowlistOnlyGroups: allowlistOnlyGroups,
2021-01-19 21:52:24 +01:00
status: &status{
enabled: true,
enableTimer: time.NewTimer(0),
2020-04-08 23:03:07 +02:00
},
clientGroupsBlock: clientGroupsBlock(cfg),
redisClient: redis,
}
res.fqdnIPCache = expirationcache.NewCacheWithOnExpired[[]net.IP](ctx, expirationcache.Options{
CleanupInterval: defaultBlockingCleanUpInterval,
}, func(ctx context.Context, key string) (val *[]net.IP, ttl time.Duration) {
return res.queryForFQIdentifierIPs(ctx, key)
})
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
if res.redisClient != nil {
go res.redisSubscriber(ctx)
2020-01-12 18:23:35 +01:00
}
2020-04-08 23:03:07 +02:00
2023-09-18 11:42:49 +02:00
err = evt.Bus().SubscribeOnce(evt.ApplicationStarted, func(_ ...string) {
go res.initFQDNIPCache(ctx)
})
2023-09-18 11:42:49 +02:00
if err != nil {
return nil, err
}
return res, nil
2020-04-08 23:03:07 +02:00
}
func (r *BlockingResolver) redisSubscriber(ctx context.Context) {
ctx, logger := r.log(ctx)
for {
select {
case em := <-r.redisClient.EnabledChannel:
if em != nil {
logger.Debug("Received state from redis: ", em)
if em.State {
r.internalEnableBlocking()
} else {
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
err := r.internalDisableBlocking(ctx, em.Duration, em.Groups)
if err != nil {
logger.Warn("Blocking couldn't be disabled:", err)
}
}
}
case <-ctx.Done():
return
}
}
}
// RefreshLists triggers the refresh of all allow/denylists in the cache
func (r *BlockingResolver) RefreshLists() error {
var err *multierror.Error
err = multierror.Append(err, r.denylistMatcher.Refresh())
err = multierror.Append(err, r.allowlistMatcher.Refresh())
return err.ErrorOrNil()
2021-02-08 21:57:59 +01:00
}
func (r *BlockingResolver) retrieveAllBlockingGroups() []string {
result := maps.Keys(r.cfg.Denylists)
result = append(result, "default")
slices.Sort(result)
return result
}
// EnableBlocking enables the blocking against the denylists
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
func (r *BlockingResolver) EnableBlocking(ctx context.Context) {
r.internalEnableBlocking()
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
if r.redisClient != nil {
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
r.redisClient.PublishEnabled(ctx, &redis.EnabledMessage{State: true})
}
}
func (r *BlockingResolver) internalEnableBlocking() {
2021-02-04 21:59:41 +01:00
s := r.status
s.lock.Lock()
defer s.lock.Unlock()
2021-02-04 21:59:41 +01:00
s.enableTimer.Stop()
s.enabled = true
s.disabledGroups = []string{}
2021-02-04 21:59:41 +01:00
evt.Bus().Publish(evt.BlockingEnabledEvent, true)
}
// DisableBlocking deactivates the blocking for a particular duration (or forever if 0).
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
func (r *BlockingResolver) DisableBlocking(ctx context.Context, duration time.Duration, disableGroups []string) error {
err := r.internalDisableBlocking(ctx, duration, disableGroups)
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
if err == nil && r.redisClient != nil {
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
r.redisClient.PublishEnabled(ctx, &redis.EnabledMessage{
State: false,
Duration: duration,
Groups: disableGroups,
})
}
return err
}
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
func (r *BlockingResolver) internalDisableBlocking(ctx context.Context, duration time.Duration,
disableGroups []string,
) error {
2021-02-04 21:59:41 +01:00
s := r.status
s.lock.Lock()
defer s.lock.Unlock()
2021-02-04 21:59:41 +01:00
s.enableTimer.Stop()
allBlockingGroups := r.retrieveAllBlockingGroups()
if len(disableGroups) == 0 {
s.disabledGroups = allBlockingGroups
} else {
for _, g := range disableGroups {
i := sort.SearchStrings(allBlockingGroups, g)
if !(i < len(allBlockingGroups) && allBlockingGroups[i] == g) {
return fmt.Errorf("group '%s' is unknown", g)
}
}
s.disabledGroups = disableGroups
}
2021-02-04 21:59:41 +01:00
s.enabled = false
2021-02-04 21:59:41 +01:00
evt.Bus().Publish(evt.BlockingEnabledEvent, false)
s.disableEnd = time.Now().Add(duration)
if duration == 0 {
2021-12-20 22:37:32 +01:00
log.Log().Infof("disable blocking for group(s) '%s'", log.EscapeInput(strings.Join(s.disabledGroups, "; ")))
2021-02-04 21:59:41 +01:00
} else {
2021-12-20 22:37:32 +01:00
log.Log().Infof("disable blocking for %s for group(s) '%s'", duration,
log.EscapeInput(strings.Join(s.disabledGroups, "; ")))
2021-02-04 21:59:41 +01:00
s.enableTimer = time.AfterFunc(duration, func() {
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
r.EnableBlocking(ctx)
log.Log().Info("blocking enabled again")
2021-02-04 21:59:41 +01:00
})
}
return nil
2020-04-08 23:03:07 +02:00
}
// BlockingStatus returns the current blocking status
2021-02-04 21:59:41 +01:00
func (r *BlockingResolver) BlockingStatus() api.BlockingStatus {
2020-04-08 23:03:07 +02:00
var autoEnableDuration time.Duration
r.status.lock.RLock()
defer r.status.lock.RUnlock()
2020-04-08 23:03:07 +02:00
if !r.status.enabled && r.status.disableEnd.After(time.Now()) {
autoEnableDuration = time.Until(r.status.disableEnd)
}
2021-02-04 21:59:41 +01:00
return api.BlockingStatus{
2020-04-08 23:03:07 +02:00
Enabled: r.status.enabled,
DisabledGroups: r.status.disabledGroups,
AutoEnableInSec: int(autoEnableDuration.Seconds()),
2020-04-08 23:03:07 +02:00
}
2020-01-12 18:23:35 +01:00
}
// returns groups, which have only allowlist entries
func determineAllowlistOnlyGroups(cfg *config.Blocking) (result map[string]bool) {
result = make(map[string]bool, len(cfg.Allowlists))
for g, links := range cfg.Allowlists {
2020-01-12 18:23:35 +01:00
if len(links) > 0 {
if _, found := cfg.Denylists[g]; !found {
result[g] = true
2020-01-12 18:23:35 +01:00
}
}
}
return
}
// sets answer and/or return code for DNS response, if request should be blocked
2021-02-22 22:29:34 +01:00
func (r *BlockingResolver) handleBlocked(logger *logrus.Entry,
request *model.Request, question dns.Question, reason string,
) (*model.Response, error) {
response := new(dns.Msg)
response.SetReply(request.Req)
r.blockHandler.handleBlock(question, response)
2020-01-12 18:23:35 +01:00
logger.Debugf("blocking request '%s'", reason)
2021-09-09 22:57:05 +02:00
return &model.Response{Res: response, RType: model.ResponseTypeBLOCKED, Reason: reason}, nil
2020-01-12 18:23:35 +01:00
}
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
// LogConfig implements `config.Configurable`.
func (r *BlockingResolver) LogConfig(logger *logrus.Entry) {
r.cfg.LogConfig(logger)
logger.Info("denylist cache entries:")
log.WithIndent(logger, " ", r.denylistMatcher.LogConfig)
2020-01-12 18:23:35 +01:00
logger.Info("allowlist cache entries:")
log.WithIndent(logger, " ", r.allowlistMatcher.LogConfig)
2020-01-12 18:23:35 +01:00
}
func (r *BlockingResolver) hasAllowlistOnlyAllowed(groupsToCheck []string) bool {
for _, group := range groupsToCheck {
if _, found := r.allowlistOnlyGroups[group]; found {
return true
}
}
return false
}
func (r *BlockingResolver) handleDenylist(ctx context.Context, groupsToCheck []string,
request *model.Request, logger *logrus.Entry,
) (bool, *model.Response, error) {
2020-04-09 23:14:08 +02:00
logger.WithField("groupsToCheck", strings.Join(groupsToCheck, "; ")).Debug("checking groups for request")
allowlistOnlyAllowed := r.hasAllowlistOnlyAllowed(groupsToCheck)
2020-01-12 18:23:35 +01:00
2020-04-09 23:14:08 +02:00
for _, question := range request.Req.Question {
domain := util.ExtractDomain(question)
logger := logger.WithField("domain", domain)
2020-01-12 18:23:35 +01:00
if groups := r.matches(groupsToCheck, r.allowlistMatcher, domain); len(groups) > 0 {
logger.WithField("groups", groups).Debugf("domain is allowlisted")
resp, err := r.next.Resolve(ctx, request)
return true, resp, err
2020-04-09 23:14:08 +02:00
}
2020-01-12 18:23:35 +01:00
if allowlistOnlyAllowed {
resp, err := r.handleBlocked(logger, request, question, "BLOCKED (ALLOWLIST ONLY)")
return true, resp, err
2020-04-09 23:14:08 +02:00
}
2020-01-12 18:23:35 +01:00
if groups := r.matches(groupsToCheck, r.denylistMatcher, domain); len(groups) > 0 {
resp, err := r.handleBlocked(logger, request, question, fmt.Sprintf("BLOCKED (%s)", strings.Join(groups, ",")))
return true, resp, err
2020-04-09 23:14:08 +02:00
}
}
return false, nil, nil
2020-04-09 23:14:08 +02:00
}
// Resolve checks the query against the denylist and delegates to next resolver if domain is not blocked
func (r *BlockingResolver) Resolve(ctx context.Context, request *model.Request) (*model.Response, error) {
ctx, logger := r.log(ctx)
2020-04-09 23:14:08 +02:00
groupsToCheck := r.groupsToCheckForClient(request)
if len(groupsToCheck) > 0 {
handled, resp, err := r.handleDenylist(ctx, groupsToCheck, request, logger)
if handled {
2020-04-09 23:14:08 +02:00
return resp, err
}
}
respFromNext, err := r.next.Resolve(ctx, request)
if err == nil && len(groupsToCheck) > 0 && respFromNext.Res != nil {
for _, rr := range respFromNext.Res.Answer {
entryToCheck, tName := extractEntryToCheckFromResponse(rr)
if len(entryToCheck) > 0 {
logger := logger.WithField("response_entry", entryToCheck)
if groups := r.matches(groupsToCheck, r.allowlistMatcher, entryToCheck); len(groups) > 0 {
logger.WithField("groups", groups).Debugf("%s is allowlisted", tName)
} else if groups := r.matches(groupsToCheck, r.denylistMatcher, entryToCheck); len(groups) > 0 {
return r.handleBlocked(logger, request, request.Req.Question[0], fmt.Sprintf("BLOCKED %s (%s)", tName,
strings.Join(groups, ",")))
2020-01-12 18:23:35 +01:00
}
}
}
}
return respFromNext, err
}
2020-01-12 18:23:35 +01:00
func extractEntryToCheckFromResponse(rr dns.RR) (entryToCheck, tName string) {
switch v := rr.(type) {
case *dns.A:
entryToCheck = v.A.String()
tName = "IP"
case *dns.AAAA:
entryToCheck = strings.ToLower(v.AAAA.String())
tName = "IP"
case *dns.CNAME:
entryToCheck = util.ExtractDomainOnly(v.Target)
tName = "CNAME"
}
return
2020-01-12 18:23:35 +01:00
}
func (r *BlockingResolver) isGroupDisabled(group string) bool {
r.status.lock.RLock()
defer r.status.lock.RUnlock()
for _, g := range r.status.disabledGroups {
if g == group {
return true
}
}
return false
}
2020-01-12 18:23:35 +01:00
// returns groups which should be checked for client's request
func (r *BlockingResolver) groupsToCheckForClient(request *model.Request) []string {
2022-09-24 00:02:58 +02:00
r.status.lock.RLock()
defer r.status.lock.RUnlock()
var groups []string
2020-01-12 18:23:35 +01:00
// try client names
for _, cName := range request.ClientNames {
for blockGroup, groupsByName := range r.clientGroupsBlock {
if util.ClientNameMatchesGroupName(blockGroup, cName) {
2020-08-24 22:15:31 +02:00
groups = append(groups, groupsByName...)
}
2020-01-12 18:23:35 +01:00
}
}
// try IP
groupsByIP, found := r.clientGroupsBlock[request.ClientIP.String()]
2020-01-12 18:23:35 +01:00
if found {
groups = append(groups, groupsByIP...)
}
for clientIdentifier, groupsByCidr := range r.clientGroupsBlock {
// try CIDR
if util.CidrContainsIP(clientIdentifier, request.ClientIP) {
groups = append(groups, groupsByCidr...)
} else if isFQDN(clientIdentifier) && r.fqdnIPCache != nil {
ips, _ := r.fqdnIPCache.Get(clientIdentifier)
if ips != nil {
for _, ip := range *ips {
if ip.Equal(request.ClientIP) {
groups = append(groups, groupsByCidr...)
}
}
}
}
}
2020-01-12 18:23:35 +01:00
if len(groups) == 0 {
// return default
groups = r.clientGroupsBlock["default"]
}
var result []string
for _, g := range groups {
if !r.isGroupDisabled(g) {
result = append(result, g)
2020-01-12 18:23:35 +01:00
}
}
sort.Strings(result)
2020-01-12 18:23:35 +01:00
return result
}
2020-01-12 18:23:35 +01:00
func (r *BlockingResolver) matches(groupsToCheck []string, m lists.Matcher,
domain string,
) (group []string) {
2020-01-12 18:23:35 +01:00
if len(groupsToCheck) > 0 {
return m.Match(domain, groupsToCheck)
2020-01-12 18:23:35 +01:00
}
return []string{}
2020-01-12 18:23:35 +01:00
}
type blockHandler interface {
handleBlock(question dns.Question, response *dns.Msg)
}
type zeroIPBlockHandler struct {
BlockTimeSec uint32
}
type nxDomainBlockHandler struct{}
type ipBlockHandler struct {
destinations []net.IP
fallbackHandler blockHandler
BlockTimeSec uint32
}
func (b zeroIPBlockHandler) handleBlock(question dns.Question, response *dns.Msg) {
var zeroIP net.IP
switch question.Qtype {
case dns.TypeAAAA:
zeroIP = net.IPv6zero
case dns.TypeA:
zeroIP = net.IPv4zero
default:
response.Rcode = dns.RcodeNameError
return
}
rr, _ := util.CreateAnswerFromQuestion(question, zeroIP, b.BlockTimeSec)
response.Answer = append(response.Answer, rr)
}
2021-01-19 21:52:24 +01:00
func (b nxDomainBlockHandler) handleBlock(_ dns.Question, response *dns.Msg) {
response.Rcode = dns.RcodeNameError
}
func (b ipBlockHandler) handleBlock(question dns.Question, response *dns.Msg) {
for _, ip := range b.destinations {
answer, _ := util.CreateAnswerFromQuestion(question, ip, b.BlockTimeSec)
2021-02-05 23:17:30 +01:00
if (question.Qtype == dns.TypeAAAA && ip.To4() == nil) || (question.Qtype == dns.TypeA && ip.To4() != nil) {
2021-02-05 23:17:30 +01:00
response.Answer = append(response.Answer, answer)
}
}
if len(response.Answer) == 0 {
// use fallback
b.fallbackHandler.handleBlock(question, response)
}
}
func (r *BlockingResolver) queryForFQIdentifierIPs(ctx context.Context, identifier string) (*[]net.IP, time.Duration) {
ctx, logger := r.logWith(ctx, func(logger *logrus.Entry) *logrus.Entry {
return log.WithPrefix(logger, "client_id_cache")
})
var result []net.IP
var ttl time.Duration
for _, qType := range []uint16{dns.TypeA, dns.TypeAAAA} {
resp, err := r.next.Resolve(ctx, &model.Request{
Req: util.NewMsgWithQuestion(identifier, dns.Type(qType)),
})
if err == nil && resp.Res.Rcode == dns.RcodeSuccess {
for _, rr := range resp.Res.Answer {
ttl = time.Duration(atomic.LoadUint32(&rr.Header().Ttl)) * time.Second
switch v := rr.(type) {
case *dns.A:
result = append(result, v.A)
case *dns.AAAA:
result = append(result, v.AAAA)
}
}
}
}
if len(result) != 0 {
logger.WithFields(logrus.Fields{
"ips": result,
"client_id": identifier,
}).Debug("resolved client IPs")
}
return &result, ttl
}
func (r *BlockingResolver) initFQDNIPCache(ctx context.Context) {
identifiers := maps.Keys(r.clientGroupsBlock)
for _, identifier := range identifiers {
if isFQDN(identifier) {
iPs, ttl := r.queryForFQIdentifierIPs(ctx, identifier)
r.fqdnIPCache.Put(identifier, iPs, ttl)
}
}
}
func isFQDN(in string) bool {
s := strings.Trim(in, ".")
return strings.Contains(s, ".")
}