Add blocking: blocklists and block/allow rules with BIND response policy zones
jarld downloads blocklists (Adblock, hosts or plain domains), checks them daily and reloads named only when a list changed; your own block and allow rules take precedence, and the most specific rule wins. Lists and rules apply per view (default: every view that offers recursion). Web UI Blocking page with name checks, jarl blocklist and jarl block, the /blocking API, blocked counts in the statistics. Database schema 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
436cd79ec9
commit
9fc23310bd
54 files changed
+4269
-85
No files matched your search
@@ -0,0 +1,93 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"jarl/internal/model"
|
||||
)
|
||||
|
||||
// checkBlocking checks blocklists and block/allow rules (docs/blocking.md).
|
||||
func (c *checker) checkBlocking() {
|
||||
viewNames := func(ids []int64) string {
|
||||
var names []string
|
||||
for _, id := range ids {
|
||||
if v := c.s.Views[id]; v != nil {
|
||||
names = append(names, v.Name)
|
||||
}
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
used := map[int64]bool{} // views (0: the server) with blocking
|
||||
// what describes where something applies, and warns when it can't
|
||||
// have any effect.
|
||||
effective := func(objType string, id int64, label string, ids []int64) []int64 {
|
||||
for _, vid := range ids {
|
||||
if c.s.Views[vid] == nil {
|
||||
c.errf("blocking_view_missing", objType, id, "%s names view %d, which doesn't exist", label, vid)
|
||||
}
|
||||
}
|
||||
views := c.s.BlockingViews(ids)
|
||||
if len(views) == 0 {
|
||||
c.warnf("blocking_no_view", objType, id, "%s has no effect: no view offers recursion, and blocking only changes answers to recursive queries", label)
|
||||
}
|
||||
var idle []int64
|
||||
for _, vid := range views {
|
||||
if !c.s.RecursionOn(vid) {
|
||||
idle = append(idle, vid)
|
||||
}
|
||||
}
|
||||
if len(idle) > 0 {
|
||||
c.warnf("blocking_no_recursion", objType, id, "%s has no effect in view %s: it doesn't offer recursion, and blocking only changes answers to recursive queries", label, viewNames(idle))
|
||||
}
|
||||
for _, vid := range views {
|
||||
used[vid] = true
|
||||
}
|
||||
return views
|
||||
}
|
||||
for _, b := range c.s.SortedBlocklists() {
|
||||
if !b.Disabled {
|
||||
effective("blocklist", b.ID, "blocklist "+b.Name, b.ViewIDs)
|
||||
}
|
||||
}
|
||||
// One rule per name and view: two would compete for the same entries
|
||||
// of the policy zone.
|
||||
seen := map[string]*model.BlockRule{} // view|name -> first rule
|
||||
for _, r := range c.s.SortedBlockRules() {
|
||||
label := "the rule for " + r.Name
|
||||
for _, vid := range effective("blockrule", r.ID, label, r.ViewIDs) {
|
||||
key := c.blockingScope(vid) + "|" + r.Name
|
||||
if first := seen[key]; first != nil && first.ID != r.ID {
|
||||
where := ""
|
||||
if v := c.s.Views[vid]; v != nil {
|
||||
where = " in view " + v.Name
|
||||
}
|
||||
c.errf("blocking_rule_duplicate", "blockrule", r.ID, "there are two rules for %s%s (%s and %s); keep one", r.Name, where, first.Action, r.Action)
|
||||
continue
|
||||
}
|
||||
seen[key] = r
|
||||
}
|
||||
}
|
||||
// Jarl writes the response-policy statement where blocking is used;
|
||||
// named allows one per view (or for the server).
|
||||
for vid := range used {
|
||||
if o := c.s.FindOption(model.ScopeView, vid, "response-policy"); vid != 0 && o != nil {
|
||||
p := c.errf("blocking_response_policy", "option", o.ID, "view %s has a response-policy option, but Jarl's blocking writes that statement there; remove the option (or the blocking that applies to this view)", c.s.Views[vid].Name)
|
||||
p.View = c.s.Views[vid].Name
|
||||
}
|
||||
}
|
||||
if o := c.s.FindOption(model.ScopeOptions, 0, "response-policy"); o != nil && len(used) > 0 {
|
||||
c.errf("blocking_response_policy", "option", o.ID, "the server has a response-policy option, which Jarl's blocking replaces wherever it is used; remove the option, or set up your policy zones with Jarl's blocking instead")
|
||||
}
|
||||
for _, z := range c.s.Zones {
|
||||
if n := strings.TrimSuffix(z.Name, "."); n == "jarl.invalid" || strings.HasSuffix(n, ".jarl.invalid") {
|
||||
c.zoneProblem(z, Error, "zone_name_reserved", "zone names under jarl.invalid are reserved for Jarl's blocking")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *checker) blockingScope(viewID int64) string {
|
||||
if v := c.s.Views[viewID]; v != nil {
|
||||
return v.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in new issue
Block a user