Client groups put devices (addresses, networks, or ACLs of them) at a blocking step: off (only your rules), normal (the default), or steps added above it, each including the lists of the steps below. Lists start at a step. In each view, a gate policy zone in front of a step's lists lets the devices below that step through (rpz-client-ip triggers with rpz-passthru), so every list is still loaded once. The IPv6 part of a gate leaves the IPv4-mapped range out: named matches IPv4 clients as ::ffff:a.b.c.d. Web UI: a Devices panel on the Blocking page and a step on each list. CLI: jarl block step, jarl block group, jarl blocklist --step. API: /blocking/steps, /blocking/groups. Database schema 8. Also: copying the state kept empty lists as empty (not nil), so reverting a changeset no longer adds restore operations for unchanged blocklists, rules and views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
332 lines
15 KiB
Markdown
332 lines
15 KiB
Markdown
# Blocking
|
|
|
|
Jarl can make named answer "this name doesn't exist" (NXDOMAIN) for ads,
|
|
trackers and malware, like Pi-hole does, for every device that uses your
|
|
server to resolve names. It has two parts:
|
|
|
|
- **Blocklists**: lists of domains published by others, which jarld
|
|
downloads and keeps up to date.
|
|
- **Your rules**: domains you block or allow yourself. They take precedence
|
|
over every list.
|
|
|
|
It uses BIND's own mechanism for this, response policy zones (RPZ), so named
|
|
does the blocking itself; jarld only prepares the lists.
|
|
|
|
- [Quick start](#quick-start)
|
|
- [Where blocking applies](#where-blocking-applies)
|
|
- [Your rules](#your-rules)
|
|
- [Blocklists](#blocklists)
|
|
- [Checking a name](#checking-a-name)
|
|
- [Pausing](#pausing)
|
|
- [Statistics](#statistics)
|
|
- [Memory and performance](#memory-and-performance)
|
|
- [How it works](#how-it-works)
|
|
- [Troubleshooting](#troubleshooting)
|
|
|
|
## Quick start
|
|
|
|
In the web UI, open **Blocking**, pick one of the well-known lists, and
|
|
click **Add list**. Or with the CLI:
|
|
|
|
```sh
|
|
jarl blocklist add hagezi-multi https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/multi.txt
|
|
jarl blocklist list # "ok" once downloaded, usually within seconds
|
|
jarl block check ads.doubleclick.net # blocked? by what?
|
|
jarl block allow cdn.example.com # something you need was blocked
|
|
```
|
|
|
|
Like every change in Jarl, adding a list or rule can be staged and reviewed
|
|
first (the web UI does this by default: apply it under **Changes**).
|
|
|
|
## Where blocking applies
|
|
|
|
Blocking changes the answers named gives when it resolves names for a
|
|
client (recursion). It doesn't change what the internet sees of your own
|
|
zones: other resolvers ask your server without recursion.
|
|
|
|
**Views.** A list or rule applies to the views you choose, or by default
|
|
to every view that offers recursion. With the usual split (an `internal`
|
|
view that resolves for your network, an `external` view that only answers
|
|
for your zones) that is just `internal`. A view that doesn't offer
|
|
recursion gets nothing, and naming one gives a warning. Without views,
|
|
blocking applies to the whole server.
|
|
|
|
**What it can't do:**
|
|
|
|
- Devices that don't use your server for DNS aren't affected. That includes
|
|
browsers using their own DNS over HTTPS, and devices with a hard-coded
|
|
resolver (8.8.8.8). A firewall rule that sends port 53 to your server, and
|
|
your network's DNS settings, are the usual answers.
|
|
- A device that validates DNSSEC itself sees a blocked name as a
|
|
validation failure rather than "doesn't exist". It's still blocked.
|
|
- A list can contain names in your own zones. They are blocked like any
|
|
other; allow them with a rule if that's not what you want.
|
|
|
|
## Your rules
|
|
|
|
A rule **blocks** or **allows** a domain and every name below it, or with
|
|
`--exact` (web UI: "only this exact name") just the domain itself.
|
|
|
|
```sh
|
|
jarl block add doubleclick.net # doubleclick.net and everything below it
|
|
jarl block add telemetry.example.com --exact # just that name
|
|
jarl block allow cdn.example.com --view internal # an exception, in one view
|
|
jarl block list
|
|
jarl block delete doubleclick.net # or: --id ID
|
|
```
|
|
|
|
**Precedence:** your rules come before every blocklist, and among your
|
|
rules the most specific one wins. With these rules:
|
|
|
|
| Rule | |
|
|
|------|-|
|
|
| block `example.com` | and below |
|
|
| allow `cdn.example.com` | exact |
|
|
|
|
`example.com` and `www.example.com` are blocked, `cdn.example.com` is
|
|
allowed, and `img.cdn.example.com` is blocked again (the allow rule was for
|
|
the exact name). An allow rule beats a blocklist entry whatever the lists
|
|
say.
|
|
|
|
There is one rule per domain and view; a second one is refused. A rule
|
|
takes effect as soon as the change is applied.
|
|
|
|
## Blocklists
|
|
|
|
**Formats.** Lists are read line by line; one list may mix formats:
|
|
|
|
| Line | Blocks |
|
|
|------|--------|
|
|
| `\|\|ads.example^` (Adblock) | the name and every name below it |
|
|
| `*.ads.example` | the name and every name below it |
|
|
| `0.0.0.0 ads.example` (hosts file, any address) | just the name |
|
|
| `ads.example` (plain domain) | just the name |
|
|
|
|
Comments (`#`, `!`), headers and blank lines are ignored. Lines that DNS
|
|
can't act on are skipped and counted: Adblock exceptions (`@@`), cosmetic
|
|
and URL rules, addresses, invalid names, and single labels (a broken list
|
|
can't block `com`). Prefer lists in Adblock format: they say which entries
|
|
cover the names below them.
|
|
|
|
**Well-known lists** (sizes in October 2026; they change):
|
|
|
|
| List | URL | Domains | Memory per view |
|
|
|------|-----|--------:|----------------:|
|
|
| Hagezi Light | `https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/light.txt` | 55,000 | ~65 MB |
|
|
| Hagezi Multi Normal | `https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/multi.txt` | 159,000 | ~190 MB |
|
|
| Hagezi Pro | `https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/pro.txt` | 195,000 | ~235 MB |
|
|
| OISD Small | `https://small.oisd.nl/` | 58,000 | ~70 MB |
|
|
| OISD Big | `https://big.oisd.nl/` | 241,000 | ~290 MB |
|
|
| StevenBlack hosts | `https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts` | 73,000 | ~45 MB |
|
|
|
|
One good list is usually enough; lists overlap a lot. Hagezi Multi Normal
|
|
is a balanced start; Light and OISD Small block less but almost never
|
|
something you need.
|
|
|
|
**Updates.** jarld checks each list every `blocking.refresh` (a day by
|
|
default) and downloads it only when the server says it changed. A new
|
|
version is checked with `named-checkzone` and replaces the old one only
|
|
when its content differs; named then reloads it. If a download or check
|
|
fails, named keeps using the previous version, jarld tries again within
|
|
the hour, and the dashboard shows the error. To update now:
|
|
|
|
```sh
|
|
jarl blocklist refresh # every list; or: refresh hagezi-multi [--force]
|
|
```
|
|
|
|
**Managing lists:**
|
|
|
|
```sh
|
|
jarl blocklist update hagezi-multi --view internal # --view auto: every view with recursion
|
|
jarl blocklist update hagezi-multi --url URL --name NEW-NAME
|
|
jarl blocklist disable hagezi-multi # stop using it, keep it (enable: back without a download)
|
|
jarl blocklist delete hagezi-multi
|
|
```
|
|
|
|
A list's state is **ok** (in use), **waiting** (not downloaded yet),
|
|
**error** (the last update failed; the previous version stays in use),
|
|
**disabled**, or **idle** (it applies to no view, so it does nothing).
|
|
|
|
## Different blocking for different devices
|
|
|
|
Client groups give some devices less or more blocking than the rest, like
|
|
Pi-hole's groups: no blocking for a work laptop, an extra list for the
|
|
kids' tablets.
|
|
|
|
**Steps** decide which lists a device gets. Each step includes the lists
|
|
of the steps below it, plus its own:
|
|
|
|
| Step | Gets |
|
|
|------|------|
|
|
| `off` (built in) | only your rules |
|
|
| `normal` (built in) | your rules and the normal lists; every device not in a group |
|
|
| `strict` (one you add) | normal, plus the lists at step `strict` |
|
|
| `very-strict` (one you add) | strict, plus the lists at step `very-strict` |
|
|
|
|
Every list starts at a step (normal unless you choose one), and every
|
|
group is at a step. Your rules apply at every step, `off` included.
|
|
|
|
```sh
|
|
jarl block step add strict
|
|
jarl blocklist update social-media --step strict
|
|
jarl block group add work 10.10.0.20 --step off
|
|
jarl block group add kids 10.10.0.50 10.10.0.51 2001:db8:1::/64 --step strict
|
|
jarl block group list
|
|
```
|
|
|
|
In the web UI, the Blocking page's **Devices** panel holds the steps and
|
|
the groups, and each list has a step.
|
|
|
|
- **Members** are addresses, networks (`10.10.0.0/24`), or ACLs made only
|
|
of those (`key`, `localnets` and `!` can't be used: blocking matches
|
|
clients by address). A device is in one group: two groups with the same
|
|
address are refused, but a more specific address in another group wins
|
|
(`10.10.0.0/24` in one group, `10.10.0.20` in another).
|
|
- **Steps only add.** A group gets everything up to its step, so "kids get
|
|
list A, guests get list B, and neither gets the other's" can't be done
|
|
with steps. That needs a view per group, each with its own lists (and
|
|
their memory).
|
|
- Lists aren't copied per step: each is loaded once, and the steps cost a
|
|
small zone each. Lists at a step no group reaches block nothing (Jarl
|
|
warns, and shows them as "no effect").
|
|
- named must see the device's own address. When a router hands out itself
|
|
as DNS server and forwards to named, every query comes from the router,
|
|
and groups can't tell devices apart. Devices also need fixed addresses
|
|
(DHCP reservations); IPv6 privacy addresses change, so add a device's
|
|
network, or rely on its IPv4 address.
|
|
- `jarl block check NAME` (and the Blocking page) answers for normal
|
|
devices, and says when a list of a higher step blocks the name.
|
|
|
|
## Checking a name
|
|
|
|
`jarl block check NAME`, or the box at the top of the Blocking page, says
|
|
for each view whether a name is blocked or allowed, and by which rule or
|
|
list entry:
|
|
|
|
```
|
|
$ jarl block check img.ads.example.com
|
|
view internal: blocked by blocklist hagezi-multi (entry *.ads.example.com)
|
|
view guest: allowed by your rule 12 (entry *.ads.example.com)
|
|
```
|
|
|
|
Views without blocking aren't listed.
|
|
|
|
In the web UI a blocked name gets an **Allow** button, and the statistics'
|
|
most blocked names link to this check.
|
|
|
|
## Pausing
|
|
|
|
To see whether blocking is what breaks a site, pause it: every name is
|
|
answered normally until the time is up (at most a day), then blocking
|
|
resumes by itself, also if jarld was restarted in between. A pause can
|
|
also last until someone resumes blocking (like Pi-hole's "indefinitely").
|
|
|
|
```sh
|
|
jarl block pause # 10 minutes; or: jarl block pause 1h
|
|
jarl block pause indefinitely
|
|
jarl block resume # end it now
|
|
```
|
|
|
|
In the web UI, **Pause blocking** is at the top of the Blocking page (with
|
|
**Until I resume** among the lengths), and the dashboard says while
|
|
blocking is paused. The lists stay loaded (the
|
|
rules zone just lets everything through), so pausing and resuming take
|
|
effect within a second. Pausing and resuming are recorded in the history
|
|
like other changes.
|
|
|
|
## Statistics
|
|
|
|
With [query statistics](stats.md) on (including `query_log`), Jarl counts
|
|
what blocking does: blocked queries over time (next to all queries) and
|
|
as a share of them, which rules and lists blocked them, the most blocked
|
|
names, and the clients with the most blocked queries (`jarl stats summary`,
|
|
`jarl stats top blocked`, `jarl stats top blocked-clients`, `jarl stats
|
|
breakdown blocklist`). Blocked answers are left out of the NXDOMAIN figures, which
|
|
are about names that really don't exist.
|
|
|
|
The Blocking page shows the last 24 hours too: a **Blocked (24h)** column
|
|
for each list, and one count for your rules together (named logs which
|
|
policy zone matched, not which rule in it).
|
|
|
|
Without statistics named doesn't log what it blocks at all.
|
|
|
|
## Memory and performance
|
|
|
|
**Memory** is the cost to watch. named keeps each list in memory, once per
|
|
view it applies to: about 0.6 KB per policy record (an Adblock entry is
|
|
two records: the name, and the names below it). The Blocking page and
|
|
`jarl blocklist list` show the estimate per list. Hagezi Pro in one view
|
|
took named from 25 MB to 260 MB in a test with BIND 9.20; while a list
|
|
reloads, the old and new versions briefly coexist.
|
|
|
|
**Answers aren't slowed**: named looks blocked names up in memory, and
|
|
answers them without asking the internet.
|
|
|
|
**Updating a list** (Hagezi Pro: download, parse, check) takes jarld about
|
|
2 seconds. While named loads the new version, the queries that the loading
|
|
thread would have answered wait, up to half a second; this only happens
|
|
when the list changed, at most once per `blocking.refresh`.
|
|
|
|
**Other changes are unaffected**: the lists' contents never go through
|
|
Jarl's apply, so adding a record takes as long with a list as without.
|
|
|
|
Downloads are limited by `blocking.max_download_mb` and
|
|
`blocking.max_entries` ([configuration.md](configuration.md#blocking)).
|
|
|
|
## How it works
|
|
|
|
For each view (or the server, without views) Jarl writes a
|
|
`response-policy` statement and the policy zones it lists, in this order:
|
|
|
|
```
|
|
response-policy {
|
|
zone "rules.jarl.invalid" min-update-interval 0; # your rules
|
|
zone "hagezi-multi.blocklist.jarl.invalid" min-update-interval 0;
|
|
} qname-wait-recurse no break-dnssec yes;
|
|
```
|
|
|
|
- Policy zones are named under `.invalid`, so they can't clash with real
|
|
zones (zone names under `jarl.invalid` are refused). They allow no
|
|
queries or transfers.
|
|
- The rules zone of each view is a file next to your zone files
|
|
(`/var/lib/bind/jarl/VIEW/rules.jarl.invalid.rpz`) and shows up in the
|
|
changeset diff like any other.
|
|
- A list's file is `/var/cache/bind/jarl/blocklists/list-ID.rpz`, shared by
|
|
the views that use it. Until the first download it is an empty policy
|
|
zone, so named always starts. Files of deleted lists are removed.
|
|
- `qname-wait-recurse no` blocks without first resolving the real answer;
|
|
`break-dnssec yes` also blocks for clients that ask for DNSSEC data;
|
|
`min-update-interval 0` applies a new version at once.
|
|
- named looks names up in a policy zone like in any zone, so a wildcard
|
|
doesn't apply below a name the zone also contains. Jarl writes the zones
|
|
so that the most specific rule still wins.
|
|
- A `response-policy` option of your own can't be combined with Jarl's
|
|
blocking in the same view: Jarl refuses the change and says so.
|
|
- Rewrites are logged to Jarl's response log when statistics read it, and
|
|
not at all otherwise.
|
|
- With client groups, a **gate** zone stands in front of the lists of each
|
|
step: `off.gate.jarl.invalid` before the normal lists,
|
|
`STEP.gate.jarl.invalid` before a step's. named checks the zones in
|
|
order and the first match decides; a gate matches the devices below its
|
|
step by address (`rpz-client-ip` triggers) with `rpz-passthru`, so they
|
|
never reach the lists behind it. A gate is never logged, and its file is
|
|
next to the rules zone (`/var/lib/bind/jarl/VIEW/strict.gate.jarl.invalid.rpz`).
|
|
For "everyone except these devices", a gate lists the address ranges
|
|
around them, at most a few hundred entries. named matches IPv4 clients as
|
|
IPv4-mapped IPv6 addresses, so the IPv6 part leaves `::ffff:0:0/96` out.
|
|
|
|
Lists and rules need the `blocking` permission ([permissions.md](permissions.md)).
|
|
|
|
## Troubleshooting
|
|
|
|
- **"no domains found in N lines"**: the URL didn't return a list (often an
|
|
HTML error page). Check it in a browser.
|
|
- **A list has no effect / is "idle"**: no view it applies to offers
|
|
recursion. Pick the view your devices use (`--view internal`).
|
|
- **Something is still answered**: `jarl block check NAME` shows whether
|
|
Jarl blocks it. If it does, the device probably doesn't use your server
|
|
(DNS over HTTPS, another resolver), or has the answer cached for a
|
|
while.
|
|
- **Something you need is blocked**: `jarl block check NAME` shows which
|
|
list; `jarl block allow NAME` overrides it.
|