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>
94 lines
3.4 KiB
Go
94 lines
3.4 KiB
Go
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 ""
|
|
}
|