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
@@ -14,8 +14,10 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
|
||||
"jarl/internal/auth"
|
||||
"jarl/internal/bind"
|
||||
"jarl/internal/blocking"
|
||||
"jarl/internal/dnsname"
|
||||
"jarl/internal/model"
|
||||
"jarl/internal/namedconf"
|
||||
@@ -64,6 +67,12 @@ type Engine struct {
|
||||
deploy *Deployer
|
||||
mu sync.Mutex // serializes applies
|
||||
Now func() time.Time
|
||||
|
||||
// Blocklist downloads (blocklists.go).
|
||||
blMu sync.Mutex // serializes downloads
|
||||
blKick chan struct{} // a list was added or changed
|
||||
HTTP *http.Client // for downloads; nil: a default client
|
||||
Limits BlocklistLimits
|
||||
}
|
||||
|
||||
// New creates an engine.
|
||||
@@ -78,6 +87,8 @@ func New(st *store.Store, tools *bind.Tools, cfg Config, log *slog.Logger) *Engi
|
||||
store: st, tools: tools, cfg: cfg, log: log,
|
||||
deploy: &Deployer{Group: cfg.FileGroup, NamedWritable: []string{cfg.Paths.ZoneDir, cfg.Paths.CacheDir}},
|
||||
Now: time.Now,
|
||||
blKick: make(chan struct{}, 1),
|
||||
Limits: DefaultBlocklistLimits(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,6 +394,7 @@ type plan struct {
|
||||
drift []DriftEntry
|
||||
configChanged bool
|
||||
reloadZones []*model.Zone
|
||||
reloadPolicy []render.PolicyZone // rules zones whose file changed
|
||||
verify map[int64]uint32
|
||||
newZones bool
|
||||
backup *Backup
|
||||
@@ -446,7 +458,7 @@ func (e *Engine) makePlan(ctx context.Context, base, work *model.State, user str
|
||||
}
|
||||
}
|
||||
for path, df := range deployed {
|
||||
if pl.out.File(path) != nil || df.Kind != render.KindZone {
|
||||
if pl.out.File(path) != nil || df.Kind != render.KindZone && df.Kind != render.KindPolicy {
|
||||
continue
|
||||
}
|
||||
old, existed, _ := readFile(path)
|
||||
@@ -484,6 +496,16 @@ func (e *Engine) makePlan(ctx context.Context, base, work *model.State, user str
|
||||
if len(work.Zones) != len(base.Zones) {
|
||||
pl.configChanged = true
|
||||
}
|
||||
// Rules zones whose content changed. New ones come with a change to
|
||||
// named.conf, and reconfig loads them.
|
||||
for _, z := range pl.out.Policy {
|
||||
if z.ListID != 0 || !changedPaths[z.File] {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(baseOut.Policy, z) {
|
||||
pl.reloadPolicy = append(pl.reloadPolicy, z)
|
||||
}
|
||||
}
|
||||
|
||||
if !e.cfg.SkipCheck {
|
||||
if err := e.stagingCheck(ctx, pl); err != nil {
|
||||
@@ -519,6 +541,16 @@ func (e *Engine) stagingCheck(ctx context.Context, pl *plan) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Blocklists as empty policy zones: their content is checked when
|
||||
// they are downloaded.
|
||||
for _, path := range sout.Blocklists {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(blocking.Placeholder()), 0o640); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
main, err := e.stagingMain(dir, sp.MainConfig())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -796,6 +828,12 @@ func (e *Engine) applyOps(ctx context.Context, cs *model.Changeset, reqs []Reque
|
||||
res.ChangesetID = cs.ID
|
||||
res.Applied = true
|
||||
e.removeDerived(pl)
|
||||
for _, op := range cs.Ops {
|
||||
if strings.HasPrefix(op.Kind, "blocklist.") {
|
||||
e.KickBlocklists()
|
||||
break
|
||||
}
|
||||
}
|
||||
e.log.Info("changeset applied", "changeset", cs.ID, "user", p.Name(), "ops", len(cs.Ops), "files", len(res.Files), "description", cs.Description)
|
||||
return res, nil
|
||||
}
|
||||
@@ -829,6 +867,9 @@ func (e *Engine) deployAndReload(ctx context.Context, pl *plan, res *Result, rel
|
||||
return &Error{Kind: KindBIND, Message: "writing configuration files failed (nothing was changed): " + err.Error()}
|
||||
}
|
||||
pl.backup = backup
|
||||
if err := e.ensureBlocklistFiles(pl.out.Blocklists); err != nil {
|
||||
e.log.Warn("creating a blocklist's empty zone file failed", "err", err)
|
||||
}
|
||||
pl.reloaded = reload
|
||||
if !reload {
|
||||
return nil
|
||||
@@ -860,6 +901,11 @@ func (e *Engine) rollback(ctx context.Context, pl *plan) {
|
||||
e.log.Error("rndc reload after restore failed", "zone", z.Name, "err", err)
|
||||
}
|
||||
}
|
||||
for _, z := range pl.reloadPolicy {
|
||||
if err := e.tools.ReloadZone(ctx, z.Name, z.View); err != nil {
|
||||
e.log.Error("rndc reload after restore failed", "zone", z.Name, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// namedView returns the view name to pass to rndc for a zone ("" without
|
||||
@@ -885,6 +931,16 @@ func (e *Engine) reload(ctx context.Context, pl *plan) ([]string, error) {
|
||||
}
|
||||
done = append(done, "reload "+zoneDesc(pl.work, z))
|
||||
}
|
||||
for _, z := range pl.reloadPolicy {
|
||||
label := "blocking rules"
|
||||
if z.View != "" {
|
||||
label += " [view " + z.View + "]"
|
||||
}
|
||||
if err := e.tools.ReloadZone(ctx, z.Name, z.View); err != nil {
|
||||
return done, fmt.Errorf("reloading %s failed: %v", label, err)
|
||||
}
|
||||
done = append(done, "reload "+label)
|
||||
}
|
||||
// named reloads zones asynchronously and reports load errors only in
|
||||
// its log, so wait until it serves the new serials.
|
||||
deadline := time.Now().Add(e.cfg.ReloadTimeout)
|
||||
@@ -1003,6 +1059,12 @@ func (e *Engine) Resync(ctx context.Context, p *auth.Principal, opts ApplyOption
|
||||
pl.verify[z.ID] = z.SOA.Serial
|
||||
}
|
||||
}
|
||||
pl.reloadPolicy = nil
|
||||
for _, z := range pl.out.Policy {
|
||||
if z.ListID == 0 {
|
||||
pl.reloadPolicy = append(pl.reloadPolicy, z)
|
||||
}
|
||||
}
|
||||
reload := !e.cfg.NoReload && !opts.NoReload
|
||||
if adopted, aerr := e.Adopted(); reload && (aerr != nil || !adopted) {
|
||||
reload = false
|
||||
|
||||
Reference in new issue
Block a user