Repository: 0xERR0R/blocky (2026-03-15T18:41:56Z)

Issue:
clientGroupsBlock group references not validated
First of all, thanks for the great project!

However, today I noticed I renamed a block group in `denylists`, but forgot to update the reference in `clientGroupsBlock`:
```yaml
blocking:
  blockType: zeroIP
  denylists:
    base:
      - https://...
    extra:
      - https://...

  clientGroupsBlock:
    default:
      - base
      - nonexisting
```

Unfortunately, it seems Blocky isn't validating the list references in `clientGroupsBlock` do actually exist as allowlist/denylist

Unless it's by design, can a validation be added? I would be happy raise a PR if you prefer (I have Golang experience, but not with this project).

## Copilot suggestion

In case it would help, Copilot's 2 cents:

To validate the referenced block groups in [clientGroupsBlock](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html), you need to check that all groups referenced in [ClientGroupsBlock](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) actually exist in either [Denylists](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) or [Allowlists](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html). Based on the codebase, here's what needs to be changed:

### Add Validation in the Configuration

The validation should be added in the [NewBlockingResolver](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) function or in the configuration validation. Here's the recommended approach:

```golang
// ...existing code...
func NewBlockingResolver(ctx context.Context,
    cfg config.Blocking,
    redis *redis.Client,
    bootstrap *Bootstrap,
) (r *BlockingResolver, err error) {
    blockHandler, err := createBlockHandler(cfg)
    if err != nil {
        return nil, fmt.Errorf("failed to create block handler: %w", err)
    }

    // Validate that all groups referenced in ClientGroupsBlock exist
    if err := validateClientGroupsBlock(cfg); err != nil {
        return nil, err
    }

    downloader := lists.NewDownloader(cfg.Loading.Downloads, bootstrap.NewHTTPTransport())
    // ...existing code...
}

func validateClientGroupsBlock(cfg config.Blocking) error {
    // Collect all defined groups
    definedGroups := make(map[string]bool)
    for group := range cfg.Denylists {
        definedGroups[group] = true
    }
    for group := range cfg.Allowlists {
        definedGroups[group] = true
    }

    // Check all referenced groups
    for clientIdentifier, groups := range cfg.ClientGroupsBlock {
        for _, group := range groups {
            if !definedGroups[group] {
                return fmt.Errorf("clientGroupsBlock references unknown group '%s' for client '%s'. Available groups: %v",
                    group, clientIdentifier, maps.Keys(definedGroups))
            }
        }
    }

    return nil
}
// ...existing code...
```

### Alternative: Add to Config Validation

You could also add this validation to the [Blocking](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) configuration struct:

```golang
// ...existing code...

// Validate checks that all referenced groups exist
func (c *Blocking) Validate() error {
    definedGroups := make(map[string]bool)
    for group := range c.Denylists {
        definedGroups[group] = true
    }
    for group := range c.Allowlists {
        definedGroups[group] = true
    }

    for clientIdentifier, groups := range c.ClientGroupsBlock {
        for _, group := range groups {
            if !definedGroups[group] {
                return fmt.Errorf("clientGroupsBlock references unknown group '%s' for client '%s'",
                    group, clientIdentifier)
            }
        }
    }

    return nil
}
// ...existing code...
```

The validation ensures that every group name listed in [ClientGroupsBlock](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) (like "gr1", "defaultGroup", "ads", etc.) must have a corresponding entry in either the [Denylists](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) or [Allowlists](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) maps. This prevents runtime errors when trying to match domains against non-existent groups.



Discussion:
This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.
Still applicable



Patch:
diff --git a/config/blocking.go b/config/blocking.go
index 52544abf8..fbb7e9d65 100644
--- a/config/blocking.go
+++ b/config/blocking.go
@@ -1,6 +1,11 @@
 package config
 
 import (
+	"fmt"
+	"maps"
+	"slices"
+	"strings"
+
 	. "github.com/0xERR0R/blocky/config/migration"
 	"github.com/0xERR0R/blocky/log"
 	"github.com/sirupsen/logrus"
@@ -88,3 +93,32 @@ func (c *Blocking) logListGroups(logger *logrus.Entry, listGroups map[string][]B
 		}
 	}
 }
+
+// validate checks blocking configuration for validity
+func (c *Blocking) validate() error {
+	if !c.IsEnabled() {
+		return nil
+	}
+
+	// Validate if all allowlists and denylists referenced
+	// in clientGroupsBlock are defined.
+	listKeys := make(map[string]bool, len(c.Denylists)+len(c.Allowlists))
+	for group := range c.Denylists {
+		listKeys[group] = true
+	}
+	for group := range c.Allowlists {
+		listKeys[group] = true
+	}
+	for clientGroupKey, clientGroupLists := range c.ClientGroupsBlock {
+		for _, listKey := range clientGroupLists {
+			if !listKeys[listKey] {
+				availableKeys := slices.Sorted(maps.Keys(listKeys))
+
+				return fmt.Errorf("clientGroupsBlock '%s' references undefined allowlist or denylist '%s'. Available: %s",
+					clientGroupKey, listKey, strings.Join(availableKeys, ", "))
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/config/config.go b/config/config.go
index 4eb96a9b8..67868abbe 100644
--- a/config/config.go
+++ b/config/config.go
@@ -648,6 +648,11 @@ func (cfg *Config) validate(logger *logrus.Entry) {
 	cfg.MinTLSServeVer.validate(logger)
 	cfg.Upstreams.validate(logger)
 
+	// Blocking validation
+	if err := cfg.Blocking.validate(); err != nil {
+		logger.Warn(err)
+	}
+
 	// DNS64 validation
 	if err := cfg.DNS64.validate(logger, &cfg.Filtering, &cfg.Caching); err != nil {
 		logger.Fatal(err)


Test patch:
diff --git a/config/blocking_test.go b/config/blocking_test.go
index 45b8660f6..c41bb0b54 100644
--- a/config/blocking_test.go
+++ b/config/blocking_test.go
@@ -86,4 +86,32 @@ var _ = Describe("BlockingConfig", func() {
 			Expect(cfg.Denylists).Should(Equal(*cfg.Deprecated.BlackLists))
 		})
 	})
+
+	Describe("validate", func() {
+		When("blocking is disabled", func() {
+			It("should not return error", func() {
+				cfg := Blocking{}
+				Expect(cfg.validate()).Should(Succeed())
+			})
+		})
+
+		When("only references existing lists", func() {
+			It("should not return error", func() {
+				Expect(cfg.validate()).Should(Succeed())
+			})
+		})
+
+		When("references non-existing lists", func() {
+			It("should return error", func() {
+				cfg := Blocking{
+					ClientGroupsBlock: map[string][]string{
+						"default": {"non-existing-group"},
+					},
+				}
+				err := cfg.validate()
+				Expect(err).Should(HaveOccurred())
+				Expect(err.Error()).Should(ContainSubstring("references undefined allowlist or denylist"))
+			})
+		})
+	})
 })