diff --git a/agent/config.go b/agent/config.go index c2c1a6f..0ebdf7e 100644 --- a/agent/config.go +++ b/agent/config.go @@ -7,6 +7,7 @@ import ( "text/template" "github.com/coredns/coredns/plugin/pkg/log" + "github.com/shoenig/donutdns/output" "github.com/shoenig/extractors/env" ) @@ -95,7 +96,7 @@ func ConfigFromEnv(e env.Environment) *CoreConfig { } // Log cc to plog. -func (cc *CoreConfig) Log(plog log.P) { +func (cc *CoreConfig) Log(logger output.Info) { log.Infof("DONUT_DNS_PORT: %d", cc.Port) log.Infof("DONUT_DNS_NO_DEBUG: %t", cc.NoDebug) log.Infof("DONUT_DNS_NO_LOG: %t", cc.NoLog) diff --git a/go.mod b/go.mod index 334f5c6..eda4465 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/coredns/caddy v1.1.1 github.com/coredns/coredns v1.8.5 + github.com/google/subcommands v1.2.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-set v0.1.6 github.com/miekg/dns v1.1.43 diff --git a/go.sum b/go.sum index e303f46..1e8063b 100644 --- a/go.sum +++ b/go.sum @@ -235,6 +235,8 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= diff --git a/main.go b/main.go index 39628bf..a5e06e8 100644 --- a/main.go +++ b/main.go @@ -2,17 +2,23 @@ package main import ( + "context" + "flag" + "os" "strconv" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/coremain" + "github.com/coredns/coredns/plugin" _ "github.com/coredns/coredns/plugin/debug" _ "github.com/coredns/coredns/plugin/forward" _ "github.com/coredns/coredns/plugin/log" "github.com/coredns/coredns/plugin/pkg/log" + "github.com/google/subcommands" "github.com/shoenig/donutdns/agent" "github.com/shoenig/donutdns/plugins/donutdns" + "github.com/shoenig/donutdns/subcmds" "github.com/shoenig/extractors/env" ) @@ -26,35 +32,48 @@ var directives = []string{ "shutdown", } -// pLog is the plugin logger associated with donutdns. -var pLog = log.NewWithPlugin(donutdns.PluginName) +// pluginLogger is the plugin logger associated with donutdns. +var pluginLogger = log.NewWithPlugin(donutdns.PluginName) -// getCC generates a CoreDNS CoreConfig file using environment variables associated with -// donutdns configuration. +// getCC generates a CoreDNS CoreConfig file using environment variables associated +// with donutdns configuration. func getCC() *agent.CoreConfig { cc := agent.ConfigFromEnv(env.OS) agent.ApplyDefaults(cc) - cc.Log(pLog) + cc.Log(pluginLogger) return cc } -func init() { +func setupCC() { // get core config from environment cc := getCC() // set plugin core config dnsserver.Port = strconv.Itoa(cc.Port) dnsserver.Directives = directives - caddy.SetDefaultCaddyfileLoader(donutdns.PluginName, caddy.LoaderFunc(func(serverType string) (caddy.Input, error) { - return caddy.CaddyfileInput{ - Filepath: donutdns.PluginName, - Contents: []byte(cc.Generate()), - ServerTypeName: donutdns.ServerType, - }, nil - })) + caddy.SetDefaultCaddyfileLoader( + donutdns.PluginName, + caddy.LoaderFunc(func(serverType string) (caddy.Input, error) { + return caddy.CaddyfileInput{ + Filepath: donutdns.PluginName, + Contents: []byte(cc.Generate()), + ServerTypeName: donutdns.ServerType, + }, nil + })) } func main() { - // launch CoreDNS; plugin configuration must be in init blocks - coremain.Run() + if len(os.Args) == 1 { + // launch CoreDNS; plugin configuration must be initialized first + setupCC() + plugin.Register(donutdns.PluginName, donutdns.Setup) + coremain.Run() + return + } + + subcommands.Register(subcmds.NewCheckCmd(), "donutdns") + + flag.Parse() + ctx := context.Background() + os.Exit(int(subcommands.Execute(ctx))) } diff --git a/output/output.go b/output/output.go new file mode 100644 index 0000000..7253e42 --- /dev/null +++ b/output/output.go @@ -0,0 +1,36 @@ +package output + +import ( + "fmt" +) + +// Info represents any type that can log via Infof. +type Info interface { + Infof(string, ...any) +} + +// Error represents any type that can log via Errorf. +type Error interface { + Errorf(string, ...any) +} + +// CLI is a wrapper around fmt.Print functions for satisfying interfaces. +type CLI struct{} + +func (o *CLI) print(msg string, args ...any) { + s := fmt.Sprintf(msg, args...) + fmt.Println(s) +} + +func (o *CLI) Infof(msg string, args ...any) { + o.print(msg, args...) +} + +func (o *CLI) Errorf(msg string, args ...any) { + o.print(msg, args...) +} + +type Logger interface { + Info + Error +} diff --git a/plugins/donutdns/plugin.go b/plugins/donutdns/plugin.go index 430ebeb..4fc3c0e 100644 --- a/plugins/donutdns/plugin.go +++ b/plugins/donutdns/plugin.go @@ -7,8 +7,8 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/request" - "github.com/hashicorp/go-set" "github.com/miekg/dns" + "github.com/shoenig/donutdns/sources" ) const ( @@ -18,11 +18,7 @@ const ( type DonutDNS struct { Next plugin.Handler - - defaultLists bool - suffix *set.Set[string] - block *set.Set[string] - allow *set.Set[string] + sets *sources.Sets } func (dd DonutDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { @@ -31,22 +27,22 @@ func (dd DonutDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms origQuery := state.Name() cleanQuery := strings.Trim(origQuery, ".") - if dd.allow.Contains(cleanQuery) { - pLog.Debugf("query for %s is explicitly allowed", cleanQuery) + if dd.sets.Allow(cleanQuery) { + pluginLogger.Debugf("query for %s is explicitly allowed", cleanQuery) return plugin.NextOrFailure(dd.Name(), dd.Next, ctx, w, r) } - if dd.block.Contains(cleanQuery) { - pLog.Debugf("query for %s is blocked by match", cleanQuery) + if dd.sets.BlockByMatch(cleanQuery) { + pluginLogger.Debugf("query for %s is blocked by match", cleanQuery) return dd.null(state.QType(), origQuery, ctx, w, r) } - if blockBySuffix(dd.suffix, cleanQuery) { - pLog.Debugf("query for %s is blocked by suffix", cleanQuery) + if dd.sets.BlockBySuffix(cleanQuery) { + pluginLogger.Debugf("query for %s is blocked by suffix", cleanQuery) return dd.null(state.QType(), origQuery, ctx, w, r) } - pLog.Debugf("query for %s is implicitly allowed", cleanQuery) + pluginLogger.Debugf("query for %s is implicitly allowed", cleanQuery) return plugin.NextOrFailure(dd.Name(), dd.Next, ctx, w, r) } @@ -62,18 +58,18 @@ func (dd DonutDNS) null(qType uint16, query string, ctx context.Context, w dns.R case dns.TypeHTTPS: answers = dd.https(query) default: - pLog.Debugf("query: %s type: %s not recognized, fallthrough", query, queryType) + pluginLogger.Debugf("query: %s type: %s not recognized, fallthrough", query, queryType) return plugin.NextOrFailure(dd.Name(), dd.Next, ctx, w, r) } - pLog.Infof("BLOCK query (%s) for %s", queryType, query) + pluginLogger.Infof("BLOCK query (%s) for %s", queryType, query) m := new(dns.Msg) m.SetReply(r) m.Authoritative = true m.Answer = answers if err := w.WriteMsg(m); err != nil { - pLog.Errorf("failed to write msg: %v", err) + pluginLogger.Errorf("failed to write msg: %v", err) return dns.RcodeServerFailure, err } diff --git a/plugins/donutdns/setup.go b/plugins/donutdns/setup.go index 47a6691..cc063b0 100644 --- a/plugins/donutdns/setup.go +++ b/plugins/donutdns/setup.go @@ -1,34 +1,24 @@ package donutdns import ( - "os" - "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/pkg/log" - "github.com/hashicorp/go-set" + "github.com/shoenig/donutdns/agent" "github.com/shoenig/donutdns/sources" - "github.com/shoenig/donutdns/sources/extract" - "github.com/shoenig/donutdns/sources/fetch" - "github.com/shoenig/ignore" ) -var pLog = log.NewWithPlugin(PluginName) - -func init() { - plugin.Register(PluginName, setup) -} +var pluginLogger = log.NewWithPlugin(PluginName) +// Setup will parse plugin config and register the donutdns plugin +// with the CoreDNS core server. +// // todo: test with TestController -func setup(c *caddy.Controller) error { +func Setup(c *caddy.Controller) error { - dd := DonutDNS{ - defaultLists: true, - suffix: set.New[string](100), - block: set.New[string](100), - allow: set.New[string](100), - } + // reconstruct the parts of CoreConfig for initializing the allow/block lists + cc := new(agent.CoreConfig) for c.Next() { _ = c.RemainingArgs() @@ -38,61 +28,55 @@ func setup(c *caddy.Controller) error { if !c.NextArg() { return c.ArgErr() } - dd.defaultLists = c.Val() == "true" - if dd.defaultLists { - defaults(dd.block) - } + cc.NoDefaults = c.Val() == "false" case "allow_file": if !c.NextArg() { return c.ArgErr() } - if filename := c.Val(); filename != "" { - custom(filename, dd.allow) - } + cc.AllowFile = c.Val() case "block_file": if !c.NextArg() { return c.ArgErr() } - if filename := c.Val(); filename != "" { - custom(filename, dd.block) - } + cc.BlockFile = c.Val() case "suffix_file": if !c.NextArg() { return c.ArgErr() } - if filename := c.Val(); filename != "" { - custom(filename, dd.suffix) - } + cc.SuffixFile = c.Val() case "allow": if !c.NextArg() { return c.ArgErr() } - dd.allow.Insert(c.Val()) + cc.Allows = append(cc.Allows, c.Val()) case "block": if !c.NextArg() { return c.ArgErr() } - dd.block.Insert(c.Val()) + cc.Blocks = append(cc.Blocks, c.Val()) case "suffix": if !c.NextArg() { return c.ArgErr() } - dd.suffix.Insert(c.Val()) + cc.Suffix = append(cc.Suffix, c.Val()) } } } - pLog.Infof("domains on explicit allow-list: %d", dd.allow.Size()) - pLog.Infof("domains on explicit block-list: %d", dd.block.Size()) - pLog.Infof("domains on suffixes block-list: %d", dd.suffix.Size()) + sets := sources.New(pluginLogger, cc) + allow, block, suffix := sets.Size() + pluginLogger.Infof("domains on explicit allow-list: %d", allow) + pluginLogger.Infof("domains on explicit block-list: %d", block) + pluginLogger.Infof("domains on suffixes block-list: %d", suffix) // Add the Plugin to CoreDNS, so Servers can use it in their plugin chain. + dd := DonutDNS{sets: sets} dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { dd.Next = next return dd @@ -101,27 +85,3 @@ func setup(c *caddy.Controller) error { // Plugin loaded okay. return nil } - -func defaults(set *set.Set[string]) { - downloader := fetch.NewDownloader(pLog) - s, err := downloader.Download(sources.Defaults()) - if err != nil { - panic(err) - } - set.InsertSet(s) -} - -func custom(filename string, set *set.Set[string]) { - // for now, everything uses the generic domain extractor - ex := extract.New(extract.Generic) - f, err := os.Open(filename) - if err != nil { - panic(err) - } - defer ignore.Close(f) - s, err := ex.Extract(f) - if err != nil { - panic(err) - } - set.InsertSet(s) -} diff --git a/plugins/donutdns/suffix.go b/plugins/donutdns/suffix.go deleted file mode 100644 index 3957d51..0000000 --- a/plugins/donutdns/suffix.go +++ /dev/null @@ -1,29 +0,0 @@ -package donutdns - -import ( - "strings" - - "github.com/hashicorp/go-set" -) - -func blockBySuffix(suffixes *set.Set[string], domain string) bool { - if suffixes.Size() == 0 { - return false - } - - domain = strings.Trim(domain, ".") - if domain == "" { - return false - } - - if suffixes.Contains(domain) { - return true - } - - idx := strings.Index(domain, ".") - if idx <= 0 { - return false - } - - return blockBySuffix(suffixes, domain[idx+1:]) -} diff --git a/sources/fetch/client.go b/sources/client.go similarity index 99% rename from sources/fetch/client.go rename to sources/client.go index 3560ff2..c6fd397 100644 --- a/sources/fetch/client.go +++ b/sources/client.go @@ -1,4 +1,4 @@ -package fetch +package sources import ( "context" diff --git a/sources/defaults.go b/sources/defaults.go index c13abc1..8a13472 100644 --- a/sources/defaults.go +++ b/sources/defaults.go @@ -38,9 +38,9 @@ func (d *Lists) All() []string { // The default set of source lists are embedded as statics/sources.json which // we then simply unmarshal at runtime. func Defaults() *Lists { - defaults := new(Lists) - if err := json.Unmarshal(sources, defaults); err != nil { + lists := new(Lists) + if err := json.Unmarshal(sources, lists); err != nil { panic(err) // defaults are embedded } - return defaults + return lists } diff --git a/sources/fetch/fetch.go b/sources/fetch.go similarity index 74% rename from sources/fetch/fetch.go rename to sources/fetch.go index 8cf0efd..2107b8a 100644 --- a/sources/fetch/fetch.go +++ b/sources/fetch.go @@ -1,12 +1,11 @@ -package fetch +package sources import ( "fmt" "net/http" - "github.com/coredns/coredns/plugin/pkg/log" "github.com/hashicorp/go-set" - "github.com/shoenig/donutdns/sources" + "github.com/shoenig/donutdns/output" "github.com/shoenig/donutdns/sources/extract" "github.com/shoenig/ignore" ) @@ -14,27 +13,27 @@ import ( // A Downloader is used to download a set of source lists. type Downloader interface { // Download all sources in Lists. - Download(*sources.Lists) (*set.Set[string], error) + Download(*Lists) (*set.Set[string], error) } type downloader struct { - pLog log.P + logger output.Logger } // NewDownloader creates a new Downloader for downloading source lists. -func NewDownloader(pLog log.P) Downloader { +func NewDownloader(logger output.Logger) Downloader { return &downloader{ - pLog: pLog, + logger: logger, } } -func (d *downloader) Download(lists *sources.Lists) (*set.Set[string], error) { - g := NewGetter(d.pLog, extract.New(extract.Generic)) +func (d *downloader) Download(lists *Lists) (*set.Set[string], error) { + g := NewGetter(d.logger, extract.New(extract.Generic)) combo := set.New[string](100) for _, source := range lists.All() { single, err := g.Get(source) if err != nil { - d.pLog.Errorf("failed to fetch source %q, skip: %s", source, err) + d.logger.Errorf("failed to fetch source %q, skip: %s", source, err) continue } combo.InsertSet(single) @@ -51,18 +50,18 @@ type Getter interface { type getter struct { client *http.Client ex extract.Extractor - plog log.P + logger output.Logger } // NewGetter creates a new Getter, using Extractor ex to extract domains. -func NewGetter(pLog log.P, ex extract.Extractor) Getter { +func NewGetter(logger output.Logger, ex extract.Extractor) Getter { return &getter{ client: client( // todo: pass in one of the upstreams // currently hard-code cloudflare for bootstrapping the sources ), - ex: ex, - plog: pLog, + ex: ex, + logger: logger, } } @@ -88,7 +87,7 @@ func (g *getter) Get(source string) (*set.Set[string], error) { return nil, fmt.Errorf("failed to extract sources: %w", err) } - g.plog.Infof("got %d domains from %q", single.Size(), source) + g.logger.Infof("got %d domains from %q", single.Size(), source) return single, nil } diff --git a/sources/fetch/fetch_test.go b/sources/fetch_test.go similarity index 93% rename from sources/fetch/fetch_test.go rename to sources/fetch_test.go index 7f90e59..2521c99 100644 --- a/sources/fetch/fetch_test.go +++ b/sources/fetch_test.go @@ -1,4 +1,4 @@ -package fetch +package sources import ( "fmt" @@ -7,7 +7,6 @@ import ( "testing" "github.com/coredns/coredns/plugin/pkg/log" - "github.com/shoenig/donutdns/sources" "github.com/shoenig/donutdns/sources/extract" "github.com/shoenig/test/must" ) @@ -41,7 +40,7 @@ func Test_Download(t *testing.T) { })) defer ts.Close() - lists := &sources.Lists{ + lists := &Lists{ Suspicious: []string{ts.URL}, Advertising: []string{ts.URL}, Tracking: []string{ts.URL}, diff --git a/sources/sets.go b/sources/sets.go new file mode 100644 index 0000000..7212c0d --- /dev/null +++ b/sources/sets.go @@ -0,0 +1,125 @@ +package sources + +import ( + "os" + "strings" + + "github.com/hashicorp/go-set" + "github.com/shoenig/donutdns/agent" + "github.com/shoenig/donutdns/output" + "github.com/shoenig/donutdns/sources/extract" + "github.com/shoenig/ignore" +) + +// Sets enables efficient look-ups of whether a domain should be allowable or blocked. +type Sets struct { + allow *set.Set[string] + block *set.Set[string] + suffix *set.Set[string] +} + +// New returns a Sets pre-filled according to cc. +func New(logger output.Logger, cc *agent.CoreConfig) *Sets { + allow := set.New[string](100) + block := set.New[string](100) + suffix := set.New[string](100) + + // initialize defaults if enabled + if !cc.NoDefaults { + defaults(block, logger) + } + + // insert individual custom allowable domains + allow.InsertAll(cc.Allows) + + // insert file of custom allowable domains + custom(cc.AllowFile, allow) + + // insert individual custom block domains + block.InsertAll(cc.Blocks) + + // insert file of custom block domains + custom(cc.BlockFile, block) + + // insert individual block domain suffixes + suffix.InsertAll(cc.Suffix) + + // insert file of custom block domain suffixes + custom(cc.SuffixFile, suffix) + + return &Sets{ + allow: allow, + block: block, + suffix: suffix, + } +} + +// Size returns the number of items in the allow, block, suffix sets. +func (s *Sets) Size() (int, int, int) { + allow := s.allow.Size() + block := s.block.Size() + suffix := s.suffix.Size() + return allow, block, suffix +} + +// Allow indicates whether domain is on the explicit allow-list. +func (s *Sets) Allow(domain string) bool { + return s.allow.Contains(domain) +} + +// BlockByMatch indicates whether domain is on the explicit block-list. +func (s *Sets) BlockByMatch(domain string) bool { + return s.block.Contains(domain) +} + +// BlockBySuffix indicates whether domain is on the suffix block-list. +func (s *Sets) BlockBySuffix(domain string) bool { + if s.suffix.Size() == 0 { + return false + } + + domain = strings.Trim(domain, ".") + if domain == "" { + return false + } + + if s.suffix.Contains(domain) { + return true + } + + idx := strings.Index(domain, ".") + if idx <= 0 { + return false + } + + return s.BlockBySuffix(domain[idx+1:]) +} + +func defaults(set *set.Set[string], logger output.Logger) { + d := NewDownloader(logger) + s, err := d.Download(Defaults()) + if err != nil { + panic(err) + } + set.InsertSet(s) +} + +func custom(filename string, set *set.Set[string]) { + if filename == "" { + return // nothing to do + } + + // for now, everything uses the generic domain extractor + ex := extract.New(extract.Generic) + f, err := os.Open(filename) + if err != nil { + panic(err) + } + defer ignore.Close(f) + + s, err := ex.Extract(f) + if err != nil { + panic(err) + } + set.InsertSet(s) +} diff --git a/plugins/donutdns/suffix_test.go b/sources/sets_test.go similarity index 83% rename from plugins/donutdns/suffix_test.go rename to sources/sets_test.go index a2209d6..b808618 100644 --- a/plugins/donutdns/suffix_test.go +++ b/sources/sets_test.go @@ -1,4 +1,4 @@ -package donutdns +package sources import ( "testing" @@ -7,7 +7,7 @@ import ( "github.com/shoenig/test/must" ) -func Test_blockBySuffix(t *testing.T) { +func TestSets_BlockBySuffix(t *testing.T) { suffixes := set.From[string]([]string{"evil.com", "ads.good.com"}) cases := []struct { @@ -35,7 +35,8 @@ func Test_blockBySuffix(t *testing.T) { for _, tc := range cases { t.Run(tc.domain, func(t *testing.T) { - result := blockBySuffix(suffixes, tc.domain) + s := &Sets{suffix: suffixes} + result := s.BlockBySuffix(tc.domain) must.Eq(t, tc.exp, result) }) } diff --git a/subcmds/check.go b/subcmds/check.go new file mode 100644 index 0000000..5a9256f --- /dev/null +++ b/subcmds/check.go @@ -0,0 +1,85 @@ +package subcmds + +import ( + "context" + "flag" + "strings" + + "github.com/google/subcommands" + "github.com/shoenig/donutdns/agent" + "github.com/shoenig/donutdns/output" + "github.com/shoenig/donutdns/sources" + "github.com/shoenig/extractors/env" +) + +const ( + checkCmdName = "check" +) + +type CheckCmd struct { + quiet bool + defaults bool + domain string +} + +func NewCheckCmd() subcommands.Command { + return new(CheckCmd) +} +func (cc *CheckCmd) Name() string { + return checkCmdName +} + +func (cc *CheckCmd) Synopsis() string { + return "Check whether a domain will be blocked." +} + +func (cc *CheckCmd) Usage() string { + return strings.TrimPrefix(` +check +Check whether domain will be blocked. +`, "\n") +} + +func (cc *CheckCmd) SetFlags(fs *flag.FlagSet) { + fs.BoolVar(&cc.quiet, "quiet", false, "silence verbose debug output") + fs.BoolVar(&cc.defaults, "defaults", false, "also check against default block lists") +} + +func (cc *CheckCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...any) subcommands.ExitStatus { + logger := new(output.CLI) + + args := f.Args() + if len(args) == 0 { + logger.Errorf("must specify domain to check command") + return subcommands.ExitUsageError + } + + if err := cc.execute(logger, args[0]); err != nil { + logger.Errorf("failure: %v", err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +func (cc *CheckCmd) execute(output *output.CLI, domain string) error { + cfg := agent.ConfigFromEnv(env.OS) + agent.ApplyDefaults(cfg) + cfg.NoDefaults = !cc.defaults + + if !cc.quiet { + cfg.Log(output) + } + + sets := sources.New(output, cfg) + switch { + case sets.Allow(domain): + output.Infof("domain %q on explicit allow list", domain) + case sets.BlockByMatch(domain): + output.Infof("domain %q on explicit block list", domain) + case sets.BlockBySuffix(domain): + output.Infof("domain %q on suffix block list", domain) + default: + output.Infof("domain %q is implicitly allowable", domain) + } + return nil +}