diff --git a/.golangci.yml b/.golangci.yml index 60630a52e..235aeb0ee 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,7 +21,7 @@ linters: - godox - gofmt - goimports - - gomnd + - mnd - gomodguard - gosec - gosimple @@ -72,7 +72,7 @@ linters: fast: false linters-settings: - gomnd: + mnd: ignored-numbers: - "0666" ginkgolinter: diff --git a/Makefile b/Makefile index e61d09b14..0a381e866 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ GO_BUILD_LD_FLAGS:=\ GO_BUILD_OUTPUT:=$(BIN_OUT_DIR)/$(BINARY_NAME)$(BINARY_SUFFIX) # define version of golangci-lint here. If defined in tools.go, go mod perfoms automatically downgrade to older version which doesn't work with golang >=1.18 -GOLANG_LINT_VERSION=v1.57.2 +GOLANG_LINT_VERSION=v1.58.2 GINKGO_PROCS?=-p diff --git a/cmd/blocking.go b/cmd/blocking.go index e77120e31..d83c620cc 100644 --- a/cmd/blocking.go +++ b/cmd/blocking.go @@ -13,9 +13,10 @@ import ( func newBlockingCommand() *cobra.Command { c := &cobra.Command{ - Use: "blocking", - Aliases: []string{"block"}, - Short: "Control status of blocking resolver", + Use: "blocking", + Aliases: []string{"block"}, + Short: "Control status of blocking resolver", + PersistentPreRunE: initConfigPreRun, } c.AddCommand(&cobra.Command{ Use: "enable", diff --git a/cmd/cache.go b/cmd/cache.go index 44d9cbc9c..858c9a325 100644 --- a/cmd/cache.go +++ b/cmd/cache.go @@ -10,8 +10,9 @@ import ( func newCacheCommand() *cobra.Command { c := &cobra.Command{ - Use: "cache", - Short: "Performs cache operations", + Use: "cache", + Short: "Performs cache operations", + PersistentPreRunE: initConfigPreRun, } c.AddCommand(&cobra.Command{ Use: "flush", diff --git a/cmd/lists.go b/cmd/lists.go index badaa8fc7..58e2197b2 100644 --- a/cmd/lists.go +++ b/cmd/lists.go @@ -11,8 +11,9 @@ import ( // NewListsCommand creates new command instance func NewListsCommand() *cobra.Command { c := &cobra.Command{ - Use: "lists", - Short: "lists operations", + Use: "lists", + Short: "lists operations", + PersistentPreRunE: initConfigPreRun, } c.AddCommand(newRefreshCommand()) diff --git a/cmd/query.go b/cmd/query.go index 609372ccd..238497410 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -14,10 +14,11 @@ import ( // NewQueryCommand creates new command instance func NewQueryCommand() *cobra.Command { c := &cobra.Command{ - Use: "query ", - Args: cobra.ExactArgs(1), - Short: "performs DNS query", - RunE: query, + Use: "query ", + Args: cobra.ExactArgs(1), + Short: "performs DNS query", + RunE: query, + PersistentPreRunE: initConfigPreRun, } c.Flags().StringP("type", "t", "A", "query type (A, AAAA, ...)") diff --git a/cmd/root.go b/cmd/root.go index eb90c287b..bcac1d9b4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,8 +10,6 @@ import ( "github.com/0xERR0R/blocky/config" "github.com/0xERR0R/blocky/log" - "github.com/0xERR0R/blocky/util" - "github.com/spf13/cobra" ) @@ -39,6 +37,7 @@ func NewRootCommand() *cobra.Command { and ad-blocker for local network. Complete documentation is available at https://github.com/0xERR0R/blocky`, + PreRunE: initConfigPreRun, RunE: func(cmd *cobra.Command, args []string) error { return newServeCommand().RunE(cmd, args) }, @@ -56,7 +55,8 @@ Complete documentation is available at https://github.com/0xERR0R/blocky`, newBlockingCommand(), NewListsCommand(), NewHealthcheckCommand(), - newCacheCommand()) + newCacheCommand(), + NewValidateCommand()) return c } @@ -65,12 +65,11 @@ func apiURL() string { return fmt.Sprintf("http://%s%s", net.JoinHostPort(apiHost, strconv.Itoa(int(apiPort))), "/api") } -//nolint:gochecknoinits -func init() { - cobra.OnInitialize(initConfig) +func initConfigPreRun(cmd *cobra.Command, args []string) error { + return initConfig() } -func initConfig() { +func initConfig() error { if configPath == defaultConfigPath { val, present := os.LookupEnv(configFileEnvVar) if present { @@ -85,7 +84,7 @@ func initConfig() { cfg, err := config.LoadConfig(configPath, false) if err != nil { - util.FatalOnError("unable to load configuration: ", err) + return fmt.Errorf("unable to load configuration file '%s': %w", configPath, err) } log.Configure(&cfg.Log) @@ -99,13 +98,13 @@ func initConfig() { port, err := config.ConvertPort(split[lastIdx]) if err != nil { - util.FatalOnError("can't convert port to number (1 - 65535)", err) - - return + return fmt.Errorf("can't convert port '%s' to number (1 - 65535): %w", split[lastIdx], err) } apiPort = port } + + return nil } // Execute starts the command diff --git a/cmd/root_test.go b/cmd/root_test.go index b5769a2d0..8f190e3cd 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -53,7 +53,7 @@ var _ = Describe("root command", func() { os.Setenv(configFileEnvVarOld, tmpFile.Path) DeferCleanup(func() { os.Unsetenv(configFileEnvVarOld) }) - initConfig() + Expect(initConfig()).Should(Succeed()) Expect(configPath).Should(Equal(tmpFile.Path)) }) @@ -62,7 +62,7 @@ var _ = Describe("root command", func() { os.Setenv(configFileEnvVar, tmpFile.Path) DeferCleanup(func() { os.Unsetenv(configFileEnvVar) }) - initConfig() + Expect(initConfig()).Should(Succeed()) Expect(configPath).Should(Equal(tmpFile.Path)) }) diff --git a/cmd/serve.go b/cmd/serve.go index 6520e3a4c..c05810dad 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -25,10 +25,12 @@ var ( func newServeCommand() *cobra.Command { return &cobra.Command{ - Use: "serve", - Args: cobra.NoArgs, - Short: "start blocky DNS server (default command)", - RunE: startServer, + Use: "serve", + Args: cobra.NoArgs, + Short: "start blocky DNS server (default command)", + RunE: startServer, + PersistentPreRunE: initConfigPreRun, + SilenceUsage: true, } } diff --git a/cmd/serve_test.go b/cmd/serve_test.go index 7a2fc2eab..301fdb7e1 100644 --- a/cmd/serve_test.go +++ b/cmd/serve_test.go @@ -42,7 +42,7 @@ var _ = Describe("Serve command", func() { os.Setenv(configFileEnvVar, cfgFile.Path) DeferCleanup(func() { os.Unsetenv(configFileEnvVar) }) - initConfig() + Expect(initConfig()).Should(Succeed()) }) errChan := make(chan error) @@ -89,7 +89,7 @@ var _ = Describe("Serve command", func() { os.Setenv(configFileEnvVar, cfgFile.Path) DeferCleanup(func() { os.Unsetenv(configFileEnvVar) }) - initConfig() + Expect(initConfig()).Should(Succeed()) }) errChan := make(chan error) diff --git a/cmd/validate.go b/cmd/validate.go new file mode 100644 index 000000000..e14dd262f --- /dev/null +++ b/cmd/validate.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "errors" + "os" + + "github.com/0xERR0R/blocky/log" + + "github.com/spf13/cobra" +) + +// NewValidateCommand creates new command instance +func NewValidateCommand() *cobra.Command { + return &cobra.Command{ + Use: "validate", + Args: cobra.NoArgs, + Short: "Validates the configuration", + RunE: validateConfiguration, + } +} + +func validateConfiguration(_ *cobra.Command, _ []string) error { + log.Log().Infof("Validating configuration file: %s", configPath) + + _, err := os.Stat(configPath) + if err != nil && errors.Is(err, os.ErrNotExist) { + return errors.New("configuration path does not exist") + } + + err = initConfig() + if err != nil { + return err + } + + log.Log().Info("Configuration is valid") + + return nil +} diff --git a/cmd/validate_test.go b/cmd/validate_test.go new file mode 100644 index 000000000..ec47ebd24 --- /dev/null +++ b/cmd/validate_test.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "github.com/0xERR0R/blocky/helpertest" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Validate command", func() { + var tmpDir *helpertest.TmpFolder + BeforeEach(func() { + tmpDir = helpertest.NewTmpFolder("config") + }) + When("Validate is called with not existing configuration file", func() { + It("should terminate with error", func() { + c := NewRootCommand() + c.SetArgs([]string{"validate", "--config", "/notexisting/path.yaml"}) + + Expect(c.Execute()).Should(HaveOccurred()) + }) + }) + + When("Validate is called with existing valid configuration file", func() { + It("should terminate without error", func() { + cfgFile := tmpDir.CreateStringFile("config.yaml", + "upstreams:", + " groups:", + " default:", + " - 1.1.1.1") + + c := NewRootCommand() + c.SetArgs([]string{"validate", "--config", cfgFile.Path}) + + Expect(c.Execute()).Should(Succeed()) + }) + }) + + When("Validate is called with existing invalid configuration file", func() { + It("should terminate with error", func() { + cfgFile := tmpDir.CreateStringFile("config.yaml", + "upstreams:", + " groups:", + " default:", + " - 1.broken file") + + c := NewRootCommand() + c.SetArgs([]string{"validate", "--config", cfgFile.Path}) + + Expect(c.Execute()).Should(HaveOccurred()) + }) + }) +}) diff --git a/config/custom_dns.go b/config/custom_dns.go index f9c8df503..8c263036e 100644 --- a/config/custom_dns.go +++ b/config/custom_dns.go @@ -75,7 +75,7 @@ func (c *CustomDNSEntries) UnmarshalYAML(unmarshal func(interface{}) error) erro result := make(CustomDNSEntries, len(parts)) for i, part := range parts { - rr, err := configToRR(part) + rr, err := configToRR(strings.TrimSpace(part)) if err != nil { return err } diff --git a/config/custom_dns_test.go b/config/custom_dns_test.go index 37efd91e2..2c5547a48 100644 --- a/config/custom_dns_test.go +++ b/config/custom_dns_test.go @@ -83,6 +83,35 @@ var _ = Describe("CustomDNSConfig", func() { Expect(aRecord.A).Should(Equal(net.ParseIP("1.2.3.4"))) }) + It("Should parse multiple ips as comma separated string", func() { + c := CustomDNSEntries{} + err := c.UnmarshalYAML(func(i interface{}) error { + *i.(*string) = "1.2.3.4,2.3.4.5" + + return nil + }) + Expect(err).Should(Succeed()) + Expect(c).Should(HaveLen(2)) + + Expect(c[0].(*dns.A).A).Should(Equal(net.ParseIP("1.2.3.4"))) + Expect(c[1].(*dns.A).A).Should(Equal(net.ParseIP("2.3.4.5"))) + }) + + It("Should parse multiple ips as comma separated string with whitespace", func() { + c := CustomDNSEntries{} + err := c.UnmarshalYAML(func(i interface{}) error { + *i.(*string) = "1.2.3.4, 2.3.4.5 , 3.4.5.6" + + return nil + }) + Expect(err).Should(Succeed()) + Expect(c).Should(HaveLen(3)) + + Expect(c[0].(*dns.A).A).Should(Equal(net.ParseIP("1.2.3.4"))) + Expect(c[1].(*dns.A).A).Should(Equal(net.ParseIP("2.3.4.5"))) + Expect(c[2].(*dns.A).A).Should(Equal(net.ParseIP("3.4.5.6"))) + }) + It("should fail if wrong YAML format", func() { c := &CustomDNSEntries{} err := c.UnmarshalYAML(func(i interface{}) error { diff --git a/docs/interfaces.md b/docs/interfaces.md index b40ab9bcb..10eb20961 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -28,6 +28,7 @@ To run the CLI, please ensure, that blocky DNS server is running, then execute ` - `./blocky query ` execute DNS query (A) (simple replacement for dig, useful for debug purposes) - `./blocky query --type ` execute DNS query with passed query type (A, AAAA, MX, ...) - `./blocky lists refresh` reloads all allow/denylists +- `./blocky validate [--config /path/to/config.yaml]` validates configuration file !!! tip diff --git a/resolver/query_logging_resolver.go b/resolver/query_logging_resolver.go index 91e880d0a..e4633d697 100644 --- a/resolver/query_logging_resolver.go +++ b/resolver/query_logging_resolver.go @@ -200,7 +200,7 @@ func (r *QueryLoggingResolver) writeLog(ctx context.Context) { r.writer.Write(logEntry) - halfCap := cap(r.logChan) / 2 //nolint:gomnd + halfCap := cap(r.logChan) / 2 //nolint:mnd // if log channel is > 50% full, this could be a problem with slow writer (external storage over network etc.) if len(r.logChan) > halfCap { diff --git a/resolver/upstream_resolver.go b/resolver/upstream_resolver.go index 605066964..f49a6b25a 100644 --- a/resolver/upstream_resolver.go +++ b/resolver/upstream_resolver.go @@ -219,7 +219,7 @@ func (r *dnsUpstreamClient) raceClients( // We don't explicitly close the channel, but since the buffer is big enough for all goroutines, // it will be GC'ed and closed automatically. - ch := make(chan exchangeResult, 2) //nolint:gomnd // TCP and UDP + ch := make(chan exchangeResult, 2) //nolint:mnd // TCP and UDP exchange := func(client *dns.Client, proto model.RequestProtocol) { msg, rtt, err := client.ExchangeContext(ctx, msg, upstreamURL)