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:
adamandClaude Opus 5.5 committed 2026-10-10 09:24:19 +00:00
1 parent 436cd79ec9
commit 9fc23310bd
54 files changed
+4269 -85

No files matched your search

+47 -14
View File
@@ -25,18 +25,39 @@ type Config struct {
TrustedProxies []string `yaml:"trusted_proxies"`
// Socket is a local unix socket for the API, where root and jarld's
// own user need no token ("" for none).
Socket string `yaml:"socket"`
Database string `yaml:"database"`
StagingDir string `yaml:"staging_dir"`
Log Log `yaml:"log"`
Session Session `yaml:"session"`
TOTP TOTP `yaml:"totp"`
BIND BIND `yaml:"bind"`
Audit Audit `yaml:"audit"`
Backup Backup `yaml:"backup"`
Stats Stats `yaml:"stats"`
ACME ACME `yaml:"acme"`
Porkbun Porkbun `yaml:"porkbun"`
Socket string `yaml:"socket"`
Database string `yaml:"database"`
StagingDir string `yaml:"staging_dir"`
Log Log `yaml:"log"`
Session Session `yaml:"session"`
TOTP TOTP `yaml:"totp"`
BIND BIND `yaml:"bind"`
Audit Audit `yaml:"audit"`
Backup Backup `yaml:"backup"`
Stats Stats `yaml:"stats"`
ACME ACME `yaml:"acme"`
Porkbun Porkbun `yaml:"porkbun"`
Blocking Blocking `yaml:"blocking"`
}
// Blocking configures blocklist downloads (docs/blocking.md). The lists
// and rules themselves are configuration in the database.
type Blocking struct {
// Refresh is how often each list is checked for changes; a list is
// only downloaded again when it changed.
Refresh Duration `yaml:"refresh"`
MaxDownloadMB int `yaml:"max_download_mb"`
MaxEntries int `yaml:"max_entries"`
}
func (b *Blocking) check() error {
if b.Refresh < Duration(time.Hour) {
return fmt.Errorf("refresh must be at least 1h (list hosts ask not to be polled more often)")
}
if b.MaxDownloadMB < 1 || b.MaxEntries < 1 {
return fmt.Errorf("max_download_mb and max_entries must be at least 1")
}
return nil
}
// Porkbun lets Jarl manage domains' name servers, glue records and DNSSEC
@@ -300,8 +321,9 @@ func Default() *Config {
Retention: StatsRetention{Minute: Duration(7 * 24 * time.Hour), Hour: Duration(90 * 24 * time.Hour), Day: Duration(730 * 24 * time.Hour)},
TopN: 100,
},
ACME: ACME{HTTPReq: HTTPReq{RequireHTTPS: true}},
Backup: Backup{Dir: "/var/backups/jarl", Retention: Duration(30 * 24 * time.Hour), KeepAtLeast: 7, WarnAfter: Duration(2 * 24 * time.Hour)},
ACME: ACME{HTTPReq: HTTPReq{RequireHTTPS: true}},
Blocking: Blocking{Refresh: Duration(24 * time.Hour), MaxDownloadMB: 64, MaxEntries: 1_000_000},
Backup: Backup{Dir: "/var/backups/jarl", Retention: Duration(30 * 24 * time.Hour), KeepAtLeast: 7, WarnAfter: Duration(2 * 24 * time.Hour)},
}
}
@@ -369,6 +391,9 @@ func (c *Config) check() error {
if err := c.Porkbun.check(); err != nil {
return fmt.Errorf("porkbun: %w", err)
}
if err := c.Blocking.check(); err != nil {
return fmt.Errorf("blocking: %w", err)
}
switch c.Log.Format {
case "text", "json":
default:
@@ -600,4 +625,12 @@ porkbun:
api_key: "" # pk1_...
secret_api_key: "" # sk1_...
domains: [] # only these domains (default: any Jarl zone the key reaches)
# Downloading blocklists (docs/blocking.md). The lists and rules are set up
# in the web UI or with 'jarl blocklist' and 'jarl block'; this only sets
# how jarld downloads them.
blocking:
refresh: 24h # check each list for changes this often (at least 1h)
max_download_mb: 64 # refuse larger downloads
max_entries: 1000000 # refuse lists with more domains
`