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
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"jarl/internal/blocking"
|
||||
)
|
||||
|
||||
// Kinds of top lists.
|
||||
@@ -21,10 +23,11 @@ const (
|
||||
KindNXDomain = "nxdomain" // names answered with NXDOMAIN
|
||||
KindServFail = "servfail" // names answered with SERVFAIL
|
||||
KindRefused = "refused" // clients that were refused
|
||||
KindBlocked = "blocked" // names blocked (docs/blocking.md)
|
||||
)
|
||||
|
||||
// Kinds lists the top-list kinds.
|
||||
var Kinds = []string{KindName, KindClient, KindNXDomain, KindServFail, KindRefused}
|
||||
var Kinds = []string{KindName, KindClient, KindNXDomain, KindServFail, KindRefused, KindBlocked}
|
||||
|
||||
// OtherZone is the zone key for names outside every local zone (answered
|
||||
// by recursion, or refused).
|
||||
@@ -99,6 +102,9 @@ type Collector struct {
|
||||
recentL []int64 // response log lines per minute, last 10 minutes
|
||||
linesMin int64
|
||||
ensureAt time.Time
|
||||
// rewritten holds queries named blocked (client#port|name) until their
|
||||
// response line arrives.
|
||||
rewritten map[string]bool
|
||||
}
|
||||
|
||||
// maxKeys bounds the per-hour maps; rarely seen keys are dropped first.
|
||||
@@ -296,8 +302,15 @@ func (c *Collector) HandleLine(line string) {
|
||||
func (c *Collector) handleLine(line string) {
|
||||
ev, ok := ParseResponse(line, c.opts.Location)
|
||||
if !ok {
|
||||
if rw, ok := ParseRewrite(line, c.opts.Location); ok {
|
||||
c.handleRewrite(rw)
|
||||
}
|
||||
return
|
||||
}
|
||||
blocked := c.rewritten[ev.Client.String()+"#"+ev.Port+"|"+ev.Name]
|
||||
if blocked {
|
||||
delete(c.rewritten, ev.Client.String()+"#"+ev.Port+"|"+ev.Name)
|
||||
}
|
||||
c.status.LinesRead++
|
||||
c.linesMin++
|
||||
t := ev.Time
|
||||
@@ -324,12 +337,14 @@ func (c *Collector) handleLine(line string) {
|
||||
tops := hours[hour]
|
||||
tops[KindName][ev.Name]++
|
||||
tops[KindClient][client]++
|
||||
switch ev.RCode {
|
||||
case "NXDOMAIN":
|
||||
switch {
|
||||
case blocked:
|
||||
// Counted by handleRewrite; not a name that doesn't exist.
|
||||
case ev.RCode == "NXDOMAIN":
|
||||
tops[KindNXDomain][ev.Name]++
|
||||
case "SERVFAIL":
|
||||
case ev.RCode == "SERVFAIL":
|
||||
tops[KindServFail][ev.Name]++
|
||||
case "REFUSED":
|
||||
case ev.RCode == "REFUSED":
|
||||
tops[KindRefused][client]++
|
||||
}
|
||||
for _, m := range tops {
|
||||
@@ -339,6 +354,48 @@ func (c *Collector) handleLine(line string) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleRewrite counts a query that blocking answered: "blocked", and
|
||||
// "blocklist:NAME" for the list (or "rules") that matched.
|
||||
func (c *Collector) handleRewrite(rw Rewrite) {
|
||||
if rw.Action == "PASSTHRU" {
|
||||
return // an allow rule: answered normally
|
||||
}
|
||||
for _, p := range c.opts.Exclude {
|
||||
if p.Contains(rw.Client) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if c.rewritten == nil || len(c.rewritten) > 10_000 {
|
||||
// Rewrites whose response line never came (named only logs
|
||||
// responses while response logging is on) don't pile up.
|
||||
c.rewritten = map[string]bool{}
|
||||
}
|
||||
c.rewritten[rw.Client.String()+"#"+rw.Port+"|"+rw.Name] = true
|
||||
minute, hour := c.minuteOf(rw.Time), c.hourOf(rw.Time)
|
||||
c.count(minute, "blocked", 1)
|
||||
c.count(minute, "blocklist:"+policySource(rw.Via), 1)
|
||||
hours := c.hours
|
||||
if hour < c.liveHour {
|
||||
hours = c.late
|
||||
}
|
||||
hours[hour] = c.hourMaps(hours, hour)
|
||||
hours[hour][KindBlocked][rw.Name]++
|
||||
}
|
||||
|
||||
// policySource names what a rewrite came from: a blocklist's name, "rules"
|
||||
// for the rules entered in Jarl, or the policy zone for others.
|
||||
func policySource(via string) string {
|
||||
via = strings.TrimSuffix(via, ".")
|
||||
if strings.HasSuffix(via, "."+blocking.RulesZone) {
|
||||
return "rules"
|
||||
}
|
||||
if i := strings.LastIndex(via, ".blocklist.jarl.invalid"); i > 0 && i+len(".blocklist.jarl.invalid") == len(via) {
|
||||
rest := via[:i]
|
||||
return rest[strings.LastIndex(rest, ".")+1:]
|
||||
}
|
||||
return via
|
||||
}
|
||||
|
||||
// shrink drops the rarest keys until the map is well under maxKeys.
|
||||
func shrink(m map[string]int64) {
|
||||
for limit := int64(1); len(m) > maxKeys*8/10; limit++ {
|
||||
|
||||
Reference in new issue
Block a user