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,136 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Blocklist is a list of domains to block, downloaded from URL and kept up
|
||||
// to date by jarld (docs/blocking.md).
|
||||
type Blocklist struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"` // a DNS label: it names the list's policy zone
|
||||
URL string `json:"url"`
|
||||
// ViewIDs are the views the list applies to; empty means every view
|
||||
// that offers recursion (see BlockingViews).
|
||||
ViewIDs []int64 `json:"view_ids"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Comment string `json:"comment"`
|
||||
Meta
|
||||
}
|
||||
|
||||
// Block rule actions.
|
||||
const (
|
||||
BlockActionBlock = "block"
|
||||
BlockActionAllow = "allow"
|
||||
)
|
||||
|
||||
// BlockRule blocks or allows a domain, and unless Exact the names below
|
||||
// it. Rules take precedence over blocklists.
|
||||
type BlockRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"` // lowercase, without the final dot
|
||||
Action string `json:"action"` // block or allow
|
||||
Exact bool `json:"exact,omitempty"`
|
||||
ViewIDs []int64 `json:"view_ids"` // as for Blocklist
|
||||
Comment string `json:"comment"`
|
||||
Meta
|
||||
}
|
||||
|
||||
// BlocklistStatus is what jarld knows about a list's downloads. It is not
|
||||
// configuration: it is kept outside changesets.
|
||||
type BlocklistStatus struct {
|
||||
ID int64 `json:"id"`
|
||||
URL string `json:"url"` // the URL last tried
|
||||
ETag string `json:"-"`
|
||||
LastModified string `json:"-"`
|
||||
CheckedAt *time.Time `json:"checked_at,omitempty"` // last attempt
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"` // last time the content changed
|
||||
Entries int `json:"entries"` // domains
|
||||
Records int `json:"records"` // policy zone records
|
||||
Skipped int `json:"skipped"` // lines that weren't usable
|
||||
SHA256 string `json:"-"`
|
||||
Error string `json:"error,omitempty"` // the last attempt failed
|
||||
}
|
||||
|
||||
// BlocklistByName finds a blocklist by name.
|
||||
func (s *State) BlocklistByName(name string) *Blocklist {
|
||||
for _, b := range s.Blocklists {
|
||||
if strings.EqualFold(b.Name, name) {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortedBlocklists returns the blocklists ordered by name.
|
||||
func (s *State) SortedBlocklists() []*Blocklist {
|
||||
out := make([]*Blocklist, 0, len(s.Blocklists))
|
||||
for _, b := range s.Blocklists {
|
||||
out = append(out, b)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// SortedBlockRules returns the rules ordered by domain, parents first.
|
||||
func (s *State) SortedBlockRules() []*BlockRule {
|
||||
out := make([]*BlockRule, 0, len(s.BlockRules))
|
||||
for _, r := range s.BlockRules {
|
||||
out = append(out, r)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
a, b := out[i], out[j]
|
||||
if a.Name != b.Name {
|
||||
return CanonicalLess(a.Name+".", b.Name+".")
|
||||
}
|
||||
if a.Exact != b.Exact {
|
||||
return !a.Exact
|
||||
}
|
||||
return a.ID < b.ID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// RecursionOn reports whether a view (0: the server without views) offers
|
||||
// recursion: its recursion option, else the server's, else BIND's default
|
||||
// (yes).
|
||||
func (s *State) RecursionOn(viewID int64) bool {
|
||||
o := s.FindOption(ScopeView, viewID, "recursion")
|
||||
if viewID == 0 || o == nil {
|
||||
if g := s.FindOption(ScopeOptions, 0, "recursion"); o == nil {
|
||||
o = g
|
||||
}
|
||||
}
|
||||
if o == nil {
|
||||
return true
|
||||
}
|
||||
switch strings.ToLower(strings.Trim(strings.TrimSpace(o.Value), `";`)) {
|
||||
case "no", "false", "0":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// BlockingViews returns the views a blocklist or rule with viewIDs applies
|
||||
// to, in view order: the given views, or when none are given every view
|
||||
// that offers recursion (blocking only affects recursive queries). Without
|
||||
// views it returns []int64{0}, the whole server, if it offers recursion.
|
||||
func (s *State) BlockingViews(viewIDs []int64) []int64 {
|
||||
views := s.OrderedViews()
|
||||
if len(views) == 0 {
|
||||
if !s.RecursionOn(0) {
|
||||
return nil
|
||||
}
|
||||
return []int64{0}
|
||||
}
|
||||
var out []int64
|
||||
for _, v := range views {
|
||||
if len(viewIDs) == 0 && s.RecursionOn(v.ID) || slices.Contains(viewIDs, v.ID) {
|
||||
out = append(out, v.ID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user