The explore page has a filter bar to type filters in (client, name or it and below, view, type, result, blocked), starting from the current filters and suggesting values from what is shown, so taking every filter out no longer means going back. The Statistics page opens it with Explore the answers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1030 lines
60 KiB
Markdown
1030 lines
60 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to Jarl are documented here. The format follows
|
||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
||
[Semantic Versioning](https://semver.org/).
|
||
|
||
## [Unreleased]
|
||
|
||
### Added
|
||
|
||
- Reverse zones made easy (docs/reverse.md):
|
||
- **Missing reverse zones**: the Zones page and `jarl zone
|
||
suggest-reverse` list the reverse zones the private views lack for
|
||
their A and AAAA records' addresses: the /24 of a private IPv4 network,
|
||
a /32 for a public address (the rest of its /24 belongs to others),
|
||
the /48 of IPv6 addresses. Create one, or all (`--create`), filled.
|
||
- **Fill**: `jarl zone create 10.0.10.0/24 --fill` (ticked in the New
|
||
zone dialog) adds a PTR record for each address the view's records use
|
||
there; `jarl zone fill-ptr` and the reverse zone's page add the ones
|
||
missing later. An address with several names gets its mail or name
|
||
server's name, else a host's.
|
||
- **The right view**: a reverse zone created without a view goes to the
|
||
private view whose records use its addresses.
|
||
- **PTR records kept in step** per zone (`jarl zone update ZONE
|
||
--auto-ptr`, zone settings → Reverse DNS): adding, changing,
|
||
disabling or deleting an A or AAAA record does the same to its PTR
|
||
record, as the Update PTR box does for one change.
|
||
- Suggestions come in two groups: **yours** (private networks, a ULA or
|
||
an IPv6 /48 this server has an address in), which Create all
|
||
creates, and **public addresses**, each a /32 or /128 to create only
|
||
if it is yours (not a web host's) or **Dismiss**. A view can turn
|
||
suggestions off (`jarl view update vps --reverse-suggestions=false`),
|
||
e.g. one only a VPS uses.
|
||
- A zone created inside another in the same view (a /24 inside a /8,
|
||
`lab.adbraun.lol` inside `adbraun.lol`) takes the parent's records
|
||
under it, which named would no longer serve from the parent; a
|
||
delegation and its glue stay.
|
||
- Zone commands take a network for a reverse zone: `jarl zone show
|
||
10.0.10.0/24`.
|
||
- **New reverse zone** (Zones page) lists the networks your records use
|
||
without a reverse zone, in every view, also where nothing is suggested
|
||
(suggestions off, dismissed, public views); pick one, change its size
|
||
(/48, /56, /64, /128 for IPv6, around the records' address) and create
|
||
it filled. With a view chosen, it also lists the addresses only other
|
||
views' records use, and fill names them from there (a VPS's AAAA in
|
||
external, its PTR in internal). `jarl zone suggest-reverse --all
|
||
[--view V]`. Both lists have a search, so hundreds of addresses stay
|
||
manageable: the public addresses fold away, show 25 at a time, and
|
||
**Dismiss these N** dismisses everything a search found in one change
|
||
(`--dismiss` takes several networks).
|
||
- Encrypted DNS: **several host names** (one certificate for all; the
|
||
health check says when the served certificate misses one), and **each
|
||
listen address gets its own views** for the host names' records
|
||
(`--address 10.0.10.5=internal`, a view checkbox per address in the
|
||
form), local to the view, with a warning when a public view would get
|
||
a private address. A view no longer ticked loses the address. The form
|
||
starts from where the records are now. The Certificates page and `jarl
|
||
cert show` say what named uses a certificate for (DNS over TLS or HTTPS,
|
||
on which addresses and ports).
|
||
- docs/encrypted-dns.md: forwarding to Quad9 or Cloudflare over TLS, how to
|
||
check it works, and why not over HTTPS.
|
||
- Comparing one name across views: `jarl zone diff adbraun.lol nas [A]`,
|
||
and a **Compare** button on each record.
|
||
|
||
- **Exploring the statistics**: jarld keeps every answer for a while
|
||
(`stats.retention.queries`, default 7 days), and every client, name,
|
||
view, record type, result and blocklist on the Statistics page leads to
|
||
an explore page for it: answers over time, the top clients, names,
|
||
views, types, results and blocklists among them (each a link that
|
||
narrows it further), and the latest answers. "NXDOMAIN in external" →
|
||
the clients asking → everything one of them asks. A filter bar types
|
||
filters in, and **Explore the answers** opens the page from the
|
||
Statistics page. `GET /stats/explore`.
|
||
- **Storage** on the dashboard: the size of Jarl's databases (and how many
|
||
answers the query log holds), and per disk Jarl uses a bar of how full it
|
||
is and how much of it the databases take. The Statistics page shows the
|
||
statistics database's size too, and its disk tiles get the same bar.
|
||
|
||
### Changed
|
||
|
||
- `jarl option set tls NAME …` (and `key`, `remote-servers`, `http`,
|
||
`dnssec-policy`, `server` …) replaces only the statement with that name:
|
||
setting `tls cloudflare` used to replace `tls quad9`. `jarl option unset
|
||
tls cloudflare` removes one by its name.
|
||
- **Compare views** and `jarl zone diff` are rebuilt around the view
|
||
you're on (docs/views.md#comparing-views): a summary per view, then
|
||
what is answered differently (a view's own records in linked zones,
|
||
drift otherwise) with **Use this everywhere**, the names only some
|
||
views have, grouped, with **Add to VIEW**, and how many are the same.
|
||
One line per record: the CLI cuts long TXT records to the terminal
|
||
(`--wide` shows them whole). `--with` picks the views to compare with.
|
||
`GET /zones/{zone}/compare` returns these sections instead of `rows`.
|
||
|
||
### Fixed
|
||
|
||
- Server options → Added by Jarl: long statements didn't wrap, and the
|
||
browser console reported a Content Security Policy violation (an inline
|
||
style the page's policy blocks).
|
||
|
||
### Upgrading from 0.5
|
||
|
||
- Tested: 0.5.2 packages with views, a linked zone, a reverse zone,
|
||
blocking, a `tls` statement and statistics, upgraded in place in Debian
|
||
13 (BIND 9.20.29): everything kept and answered as before, the new
|
||
features worked on the upgraded data, and a 0.5.2 backup restored.
|
||
- The database gets two new versions (12 and 13), applied when jarld
|
||
starts; a 0.5 jarld can't open it afterwards, so take a backup first (`sudo
|
||
jarld backup`). Nothing changes until you turn `auto_ptr` on for a
|
||
zone.
|
||
- The statistics database gets a new version too (the query log); 0.5
|
||
can't open it afterwards either. It starts empty and fills from then on.
|
||
|
||
## [0.5.2] - 2026-10-12
|
||
|
||
### Added
|
||
|
||
- Mail reverse DNS check: the addresses of the public zones' MX hosts and
|
||
SPF records are looked up as the internet sees them, every 6 hours: a
|
||
PTR record whose name resolves back to the address, which Gmail and
|
||
Outlook require. It says who runs the reverse zone (your provider) and
|
||
what to set there, and notes when Jarl has its own reverse zone for the
|
||
address, which only your clients see. The Exposure page (Check now),
|
||
`jarl exposure mail` and Prometheus `jarl_mail_reverse_dns_ok`.
|
||
- Added by Jarl: the Server options page, `jarl option generated` and
|
||
`GET /options/generated` list what Jarl writes into named.conf by
|
||
itself (keys.conf, the statistics channel and logging, blocking's
|
||
response-policy, the view keys), why, and what controls each.
|
||
- `stats.rate_limit_log` (default true): `false` stops Jarl adding named's
|
||
`query-errors` category to its log channel when rate limiting is set.
|
||
|
||
### Changed
|
||
|
||
- Option values with a list of three or more elements (allow-query,
|
||
forwarders, listen-on, ...) are shown one element per line.
|
||
|
||
## [0.5.1] - 2026-10-12
|
||
|
||
### Added
|
||
|
||
- Encrypted DNS health: jarld checks the DoT and DoH listeners every 10
|
||
minutes as a client would (the certificate is for the host name,
|
||
current and the one Jarl last wrote; a question gets an answer) and
|
||
says what is wrong: nothing listening, a firewall, the wrong or an
|
||
older certificate. On the Encrypted DNS page (Check now), the
|
||
dashboard, `jarl encrypted-dns health` and Prometheus
|
||
`jarl_encrypted_dns_up`.
|
||
- Comparing a zone's views: the Compare views tab and `jarl zone diff`
|
||
show the names and types that differ between the views that have a
|
||
zone, linked or not, with "Use this version" to share one view's
|
||
version. `GET /zones/{zone}/compare`.
|
||
|
||
### Changed
|
||
|
||
- `--local` (and `"local": true`) works on zones that aren't linked too:
|
||
their records are local anyway. `--shared` there is still refused.
|
||
- When `certificates.dir` changes, jarld points the `tls` statements
|
||
encrypted DNS wrote at the new place when it starts; named kept the old
|
||
copies before, which renewals no longer updated.
|
||
|
||
### Fixed
|
||
|
||
- Sharing a view's version of a name that was shared already (Use this
|
||
version on the shared side) didn't replace another view's override; it
|
||
said there was nothing to do.
|
||
|
||
## [0.5.0] - 2026-10-12
|
||
|
||
### Upgrading from 0.4
|
||
|
||
- The database gets three new versions (internet exposure, certificates,
|
||
linked zones), applied when jarld starts; a 0.4 jarld can't open it
|
||
afterwards, so take a backup first (`sudo jarld backup`). Backups made
|
||
by 0.4 restore into this version. Tested: 0.4.0 packages with views,
|
||
zones, records, blocking and statistics, upgraded in place and
|
||
restored from a 0.4.0 backup.
|
||
|
||
- Statistics per view start with this version: earlier figures are for
|
||
all views together.
|
||
|
||
- New exposure warnings (docs/exposure.md) appear for views reachable from
|
||
the internet, e.g. "no rate-limit" or "version visible" on an external
|
||
view: fix them from the Exposure page, or acknowledge them. A change
|
||
that newly opens a public view up (an open resolver, say) now needs
|
||
`--accept-risk`, or the checkbox on the Changes page.
|
||
|
||
- Once you use address ranges, the database can't go back to 0.4: its
|
||
parser doesn't know them. Remove the ranges first if you need to
|
||
downgrade.
|
||
|
||
### Changed
|
||
|
||
- The web UI's top bar had grown too long, and the user name, Users and
|
||
Log out ended up on a second line on the left. The server's
|
||
configuration and security pages (Server options, ACLs, TSIG keys,
|
||
Exposure, Certificates, Encrypted DNS, Audit log) are now in a
|
||
**Server** menu, and Account, Users and Log out in a menu under your
|
||
name. On narrow screens the search box and your name stay together on
|
||
the right.
|
||
|
||
### Fixed
|
||
|
||
- "update PTR" when editing a record (`jarl record update ID --ptr`) did
|
||
nothing unless the name or address changed too, and the web UI didn't
|
||
even send it alone: a reverse zone created after the record never got
|
||
its PTR. Alone, it now adds the PTR record if it is missing (or enables
|
||
it). For records of linked zones, the reverse zones of every linked view
|
||
count, not only the record's own view.
|
||
|
||
- Option values in the web UI looked different from the editor: a long
|
||
line wrapped back to the left edge, losing its indentation, and the
|
||
column changed width when Edit opened. Values now keep their layout
|
||
(a long line scrolls), with the same width and padding as the editor.
|
||
The "{ } around the list are optional" hint only shows for options that
|
||
end in a list (not rate-limit, which only has one inside).
|
||
- Query statistics skipped every query signed with a TSIG key (named
|
||
writes "/key NAME" after the client's port), such as queries with a
|
||
view's key: they are counted now.
|
||
- A changeset with a staged record delete could not be applied after that
|
||
record was deleted elsewhere (e.g. with the CLI), and it was hard to see
|
||
which operation to remove: the error only said "record 832 does not
|
||
exist". A staged delete of a record that is already gone now counts as
|
||
done (with a warning), and the other operations that no longer apply
|
||
(an edit or disable of a deleted record, a change to an object changed
|
||
since) are named with their number and summary. The Changes page marks
|
||
that operation and offers to remove it right in the error.
|
||
|
||
### Added
|
||
|
||
- Statistics per view ([docs/stats.md](docs/stats.md#views)): the
|
||
Statistics page has a toggle per view, with All and None, remembered in
|
||
the browser, to see some views only or several together (linked views,
|
||
say). Answers, result codes, record types, zones, blocking, rate
|
||
limiting and the top lists follow it; named's own counters stay for the
|
||
whole server, and the page says so. `jarl stats --view`, API
|
||
`?views=`. Per-view figures start with this version.
|
||
- DNS over TLS and HTTPS ([docs/encrypted-dns.md](docs/encrypted-dns.md)):
|
||
`jarl encrypted-dns setup --hostname NAME --address ADDR` (and the
|
||
Encrypted DNS page) obtains a certificate for the host name if needed,
|
||
checks that nothing else listens on port 853 and the DoH port (443 by
|
||
default) at those addresses, offering a free port, and stages named's
|
||
`tls` statement and listeners as one change, keeping port 53 where it
|
||
was, plus the host name's records in your private views (local to the
|
||
view in a linked zone). The page and `jarl encrypted-dns` say what to
|
||
give Android, Linux, Firefox, Chrome and Windows. A `tls` statement whose
|
||
files are missing is refused (`tls_file_missing`), and a certificate
|
||
named uses can't be deleted.
|
||
- Linked zones ([docs/views.md](docs/views.md#linked-zones)): zones of the
|
||
same name in different views can be linked, and Jarl keeps their shared
|
||
records in step: a change in one view reaches the others in the same
|
||
changeset (diff, permissions, history and undo cover every view). Each
|
||
view can still have records of its own (`--local`, "Only here"), which
|
||
override the shared records of that name and type there, and keeps its
|
||
own zone options. Linking existing zones changes no answer: what is the
|
||
same everywhere becomes shared, the rest stays local, and a view's
|
||
version can be shared later (`--shared`, "Share"). `jarl zone link` and
|
||
`unlink`, `zone create --link-with VIEW`, `record add|set|update
|
||
--local|--shared`, a LINK column in `record list`, "Keep them linked"
|
||
when copying a zone in the web UI, `POST /zones/{zone}/link` and
|
||
`/unlink`. ACME challenges are written once per link, and `--view all`
|
||
changes a link once.
|
||
- TLS certificates ([docs/certificates.md](docs/certificates.md)): Jarl
|
||
obtains certificates from Let's Encrypt or any ACME CA (account set up
|
||
in jarl.yaml under `acme.account`) and renews them itself, from the CA's
|
||
renewal information (ARI) or two thirds into their lifetime, with
|
||
back-off after failures. It answers the DNS-01 challenges in a zone it
|
||
serves, or through Porkbun's DNS API while Porkbun serves the domain;
|
||
`auto` decides from the domain's public name servers at every renewal,
|
||
so moving the domain to your own name servers needs no change. A CNAME
|
||
on `_acme-challenge` into a zone Jarl serves is followed, for domains
|
||
hosted elsewhere. A certificate is used for jarld's HTTPS ("web") and/or
|
||
written for named (`<bind.config_dir>/tls/NAME/`, "dns"; named rereads
|
||
it on renewal), and certificates you renew yourself can be used too
|
||
(reread when their files change). Certificates page and dashboard card
|
||
in the web UI, `jarl cert` (check, create, issue, update, delete,
|
||
account), `/api/v1/certificates`, Prometheus
|
||
`jarl_certificate_expiry_timestamp_seconds`, `jarl_certificate_failing`.
|
||
- jarld serves HTTPS and plain HTTP on the same ports once it has a
|
||
certificate (`tls.cert_file`, or one with the web use, which takes
|
||
effect without a restart): plain HTTP from the server itself and from
|
||
trusted proxies is served as before, browsers elsewhere are redirected
|
||
to HTTPS and API requests get `https_required`. Before, with
|
||
`tls.cert_file`, plain HTTP got an error even from localhost.
|
||
- Internet exposure ([docs/exposure.md](docs/exposure.md)): Jarl works out
|
||
which views are reachable from the internet (match-clients ending in a
|
||
catch-all; for a server without views, open allow-query and a
|
||
non-private listen address), with a per-view override, and checks them:
|
||
response rate limiting, open resolvers, ANY answers (minimal-any), cache
|
||
snooping, open NOTIFY and zone transfers, BIND version and host name,
|
||
management interfaces. Fixes are staged like any change, including
|
||
response rate limiting from ISC's example in log-only mode, with your
|
||
secondaries exempt, and a later "start enforcing". With statistics on,
|
||
Jarl reads named's rate limiting log lines and shows what was (or would
|
||
have been) limited: counts, client networks and names (Exposure page,
|
||
`jarl stats breakdown rrl`, top lists `ratelimited`, Prometheus
|
||
`jarl_ratelimit_logged_total`). A change that newly opens a public view
|
||
up must be confirmed (checkbox on the Changes page, `--accept-risk`, API
|
||
`accept_risk`; 409 `confirm_risk` otherwise). Findings can be
|
||
acknowledged with a reason: they stay visible as info, with who, when
|
||
and why. Exposure page and dashboard card in the web UI, `jarl
|
||
exposure` (fix, ack, unack), `GET /exposure`.
|
||
- `jarl changeset show`, `diff`, `apply`, `discard` and `remove` take the
|
||
changeset from `-C` or `JARL_CHANGESET` when no ID is given
|
||
(`jarl changeset apply`). A changeset from `JARL_CHANGESET` is now
|
||
checked before it is used, for staging too: it must exist, be open and
|
||
be yours, so a stale export (an applied changeset, or an ID that later
|
||
turned into someone else's web UI draft) can't silently take your
|
||
changes; the error says what to do. Commands say on stderr which
|
||
changeset from `JARL_CHANGESET` they use, and `apply`/`discard` remind
|
||
you to `unset JARL_CHANGESET`. `-C` and ID arguments are used as given.
|
||
- Address ranges such as `10.0.10.101-10.0.10.199` or
|
||
`2001:db8::100-2001:db8::1ff` (also `!` negated, and with spaces around
|
||
the dash) wherever an address match list is used: ACLs, views'
|
||
match-clients and match-destinations, options such as allow-query, and
|
||
blocking's client groups ([docs/concepts.md](docs/concepts.md)). Jarl
|
||
keeps the range as written and writes the fewest prefixes that cover
|
||
exactly that range to named.conf, with the range as a comment. The web
|
||
UI shows what a range becomes while you type it (`GET
|
||
/catalog/ranges`). Mistakes are refused when entered: an end that isn't
|
||
a full address, the start after the end, IPv4 mixed with IPv6, a range
|
||
in an option that takes no address match list (forwarders). A range
|
||
covered by an earlier element is reported as never matching, a range of
|
||
every address counts as "any" for the open resolver and open transfer
|
||
checks, and an IPv6 range in the IPv4-mapped space gets a warning. ACL
|
||
names can no longer look like a range.
|
||
- Prometheus metrics at `/metrics` ([docs/metrics.md](docs/metrics.md)),
|
||
set up in the new `metrics:` section of jarl.yaml (off by default).
|
||
`metrics.allow` lists the addresses that may scrape (loopback by
|
||
default), `metrics.token` lets anyone who sends it as a bearer token
|
||
scrape, and `metrics.listen` adds addresses that serve only `/metrics`
|
||
(for a Prometheus server elsewhere). Root and jarld's own user can also
|
||
scrape on the local socket.
|
||
- Health, always: named up, recursive and TCP clients against their
|
||
limits, adopted, open changesets, last apply, validation problems by
|
||
severity, drifted files, object counts, the last backup (and failure,
|
||
and whether it is stale), blocking paused, each blocklist's entries,
|
||
last check and change and whether it fails, the DNSSEC phase of every
|
||
signed zone, and when API tokens expire.
|
||
- With query statistics on: named's counters (queries, result codes,
|
||
record types, UDP/TCP, authoritative/recursive, rate limiting, upstream
|
||
round-trip times, DNSSEC validation, cache hits) and Jarl's (logged
|
||
responses per view and zone, blocked queries per blocklist), as
|
||
counters since jarld started, plus named's memory, CPU, threads, cache
|
||
and sockets, and the collector's health.
|
||
- Example queries and alert rules, checked with promtool.
|
||
|
||
## [0.4.0] - 2026-10-11
|
||
|
||
### Upgrading from 0.3
|
||
|
||
- Back up first (`sudo jarld backup`).
|
||
- When jarld first starts, it rewrites named's configuration: options
|
||
move into their new fixed order, and every view gets its view key in
|
||
`match-clients` and `keys.conf`. It is checked with named-checkconf
|
||
before named reloads, like any apply. Files you changed by hand are left
|
||
alone; the log then says to run `jarl resync`.
|
||
- The systemd unit changed (it allows `AF_NETLINK`). The package replaces
|
||
it; an override that adds `AF_NETLINK` (`systemctl edit jarl`) does no
|
||
harm and can be removed.
|
||
- The database moves to schema 8 (disabled zones, client groups) as
|
||
jarld starts.
|
||
- If statistics showed a spike after views were created or changed, see
|
||
"Statistics" under Fixed: the bad minutes can be removed from
|
||
`/var/lib/jarl/stats.db` (minute rows have `res = 'm'`; the hours and
|
||
days that include them are `'h'` and `'d'`).
|
||
|
||
### Added
|
||
|
||
- Different blocking for different devices (docs/blocking.md), like
|
||
Pi-hole's groups, in one view and without loading any list twice.
|
||
Client groups put devices (addresses, networks or ACLs of them) at a
|
||
blocking step: `off` (only your rules), `normal` (the default), or steps
|
||
you add above it, each including the lists of the steps below. Lists
|
||
start at a step. Jarl puts a small "gate" policy zone in front of each
|
||
step's lists that lets the devices below that step through. Web UI: a
|
||
Devices panel on the Blocking page and a step on each list. CLI: `jarl
|
||
block step`, `jarl block group`, `jarl blocklist update --step`. API:
|
||
`/blocking/steps`, `/blocking/groups`. Database schema 8.
|
||
- View keys: every view gets a TSIG key, `jarl-view-NAME`, at the front of
|
||
its match-clients, so a query signed with it lands in that view from any
|
||
address. Jarl signs its delegation check's queries to this server's own
|
||
addresses with the key of the zone's view: before, they got whichever
|
||
view matched the server's own address (usually internal), so the check
|
||
could report the wrong serial for the external view. `jarl view key
|
||
VIEW` shows a key, for `dig -k` (docs/views.md). The first apply after
|
||
upgrading adds the keys to named.conf and keys.conf.
|
||
- Server statistics: named's memory (resident, and its own count) and
|
||
CPU use, its cache size, sockets, and recursive and TCP clients against
|
||
their limits; the host's memory and load; free space where zone files,
|
||
logs and Jarl's data are. Memory and CPU are kept over time like the
|
||
other statistics. Statistics page → Server, `jarl stats server`, API
|
||
`/stats/server`. Read from named's statistics channel, `rndc status` and
|
||
/proc, with no extra privileges.
|
||
- CLI: the record commands that change records take several views,
|
||
`--view internal,external`, or `--view all`, and make the change in each,
|
||
as one changeset (all views or none).
|
||
- Blocking can be paused until resumed, like Pi-hole's "indefinitely":
|
||
`jarl block pause indefinitely`, **Until I resume** on the Blocking page,
|
||
`{"indefinitely": true}` in the API.
|
||
- Disabled zones: a zone can be disabled instead of deleted. named stops
|
||
loading it (its zone statement is left out of named.conf), while Jarl
|
||
keeps it with its records, options and history, still editable;
|
||
enabling it serves it as it is then. Disabling says what clients get
|
||
instead (recursion, or REFUSED). Signed zones can't be disabled. Web
|
||
UI: Disable/Enable on the zone page, a badge in the zone list. CLI:
|
||
`jarl zone disable|enable`. API: `PATCH /zones/{zone}` with
|
||
`{"disabled": true}`. Database schema 7.
|
||
- Blocking (docs/blocking.md): named answers "doesn't exist" for ads,
|
||
trackers and malware, using BIND's response policy zones.
|
||
- Blocklists in Adblock, hosts or plain-domain format, downloaded by jarld
|
||
and checked daily (`blocking.refresh`). A list is downloaded only when
|
||
the server says it changed, checked with named-checkzone, and reloaded
|
||
only when its content differs; a failed update keeps the previous
|
||
version and shows on the dashboard.
|
||
- Your own block and allow rules, for a domain and the names below it or
|
||
the exact name. Rules take precedence over every list, and the most
|
||
specific rule wins.
|
||
- Lists and rules apply to chosen views, or to every view that offers
|
||
recursion. They are configuration: staged, reviewed and reverted like
|
||
any change.
|
||
- Checking a name says whether it is blocked and by which rule or list
|
||
entry, with an Allow button in the web UI.
|
||
- Web UI: a Blocking page, with well-known lists to pick and named's
|
||
memory per list. CLI: `jarl blocklist` and `jarl block`. API:
|
||
`/blocking/lists`, `/blocking/rules`, `/blocking/check`.
|
||
- Pausing: `jarl block pause [DURATION]` or the Blocking page lets every
|
||
name through for a while (at most a day); blocking resumes by itself.
|
||
The lists stay loaded, so it takes effect at once.
|
||
- Statistics: blocked queries over time (a line on the queries chart)
|
||
and their share, blocked per list, the most blocked names and the
|
||
clients with the most blocked queries (`jarl stats top blocked`,
|
||
`top blocked-clients`). Blocked answers no longer count as names that
|
||
don't exist.
|
||
- The view choice in the web UI's forms is a compact dropdown, however
|
||
many views there are.
|
||
- A `blocking` permission; database schema 6.
|
||
|
||
### Changed
|
||
|
||
- Web UI: each view has its own page, like a zone: Settings (match lists,
|
||
comment, colour), Options and Zones tabs (with **New zone in VIEW**),
|
||
and Delete. The Views page lists them in order, with the arrows that
|
||
reorder them; view links everywhere open the view's page.
|
||
- Web UI: A and AAAA records have a **+ CNAME** button that fills in the
|
||
add form with a CNAME pointing to the record's name, leaving only the
|
||
alias's name to type. It shows when the row is hovered or focused
|
||
(always on touch screens).
|
||
- Blocking page: how many queries each blocklist blocked in the last 24
|
||
hours, and your rules together, with statistics on (linking to the
|
||
Statistics page).
|
||
- Options are kept in one fixed order, in named.conf, the web UI and the
|
||
CLI: grouped by topic (files, listening, access control, recursion and
|
||
forwarding, DNSSEC, transfers and notify, then the rest), alphabetical
|
||
within a group. The same option is now in the same place in every view
|
||
and zone. The first apply after upgrading reorders named.conf (a diff
|
||
that only moves lines). Top-level statements keep their order.
|
||
- Options that end in a list (`allow-transfer`, `forwarders`,
|
||
`listen-on`, ...) can be given without their braces, in the web UI, CLI
|
||
and API: `10.0.10.53; key xfer` becomes `{ 10.0.10.53; key xfer; }`.
|
||
Before, only some server options (those edited one element per line)
|
||
added them.
|
||
- The dashboard's configuration checks include named-checkconf's own
|
||
warnings about the running configuration (such as `both "recursion no;"
|
||
and "allow-recursion" active`), which before only showed while applying.
|
||
The CLI now prints them after an apply too.
|
||
- Statistics page: cards and tiles fill their rows instead of leaving one
|
||
alone beside an empty space, and the queries and failed answers charts
|
||
have the same margins, so their time axes line up.
|
||
- Dashboard: options have their own tile; "0 TSIG keys · 22 options" read
|
||
as if the options belonged to the keys.
|
||
- Web UI times use the 24-hour clock.
|
||
- Diffs of named.conf say which block each change is in, after the `@@`
|
||
line numbers (`@@ -50,6 +49,8 @@ view "internal" {`), like git does
|
||
with function names. Moving an option from the server's options into a
|
||
view no longer looks like two unrelated edits.
|
||
- Web UI options (server, top-level, view and zone): values with blocks,
|
||
such as logging, are shown and edited laid out as in named.conf, over
|
||
several lines with indentation. The editor grows with its content, Tab
|
||
indents and Enter keeps the indentation (Esc, then Tab, moves on). With
|
||
a draft open, the tables show the draft's values, so a staged edit stays
|
||
visible after Save.
|
||
- The pending-changes banner has an Apply now button: it applies your
|
||
draft without going to the Changes page, and stays on the page you're
|
||
on. If the changes are refused, a dialog says why and links to Changes.
|
||
- Blocking page: blocklists and rules have an Edit button (name, URL,
|
||
views, comment; a rule's action and scope). The list button that
|
||
downloads now is called Refresh, like `jarl blocklist refresh`.
|
||
- Records table: disabled records are hatched and have a "disabled" badge
|
||
next to their name, instead of only a muted colour and a badge in the
|
||
comment column.
|
||
|
||
### Fixed
|
||
|
||
- Reverting a changeset added needless "restore" operations for
|
||
blocklists, rules and views that hadn't changed: copying the state
|
||
turned their empty view lists into missing ones.
|
||
- Under the shipped systemd unit, jarld couldn't list the host's
|
||
addresses (it needs a netlink socket, which `RestrictAddressFamilies`
|
||
didn't allow), so the name server checks didn't recognize this server's
|
||
own addresses. The Name servers tab then queried them like another
|
||
server's, and the delegation check didn't sign those queries with the
|
||
view's key: with views, both got the view matching the server's own
|
||
address (usually internal) and reported a wrong serial. The unit now
|
||
allows `AF_NETLINK`, and without it jarld reads the addresses from
|
||
/proc.
|
||
- Statistics: after a reconfig that added, removed or recreated views
|
||
(creating the first view, for instance), a minute could count everything
|
||
since named started: millions of queries in an hour. A per-view counter
|
||
(cache, upstream times, validation) going down was taken for a restart
|
||
of named. Now only a new boot time means a restart, and a counter that
|
||
goes down counts 0 for that minute.
|
||
- Blocking page, checking a name: when your own block rule is for that very
|
||
name, it offers to remove the rule instead of an allow rule, which was
|
||
refused as a second rule for the same name. It also names the rule that
|
||
matched rather than the policy zone entry.
|
||
- docs/validation.md lacked the `disabled_conflict` warning added in 0.3.0.
|
||
|
||
## [0.3.0] - 2026-10-09
|
||
|
||
### Added
|
||
|
||
- Disabled records: a record can be disabled instead of deleted. It stays
|
||
in Jarl with its ID, TTL and comment but is left out of the zone (the
|
||
zone file lists it as a comment), and enabling it puts it back exactly
|
||
as it was. Web UI: a Disable/Enable button on each record, disabled
|
||
records greyed out with a badge, a filter for them, and a count in the
|
||
zone list. CLI: `jarl record disable|enable` (by ID or by name, type and
|
||
data, `--ptr` for the PTR record, `--missing-ok`), `record list
|
||
--disabled`, and `disable:`/`enable:` entries for `jarl apply -f`. API:
|
||
`POST /records/{id}/disable|enable`, `POST
|
||
/zones/{zone}/records/disable|enable`, a `disabled` field on records,
|
||
`?disabled=` on record lists and `record.disable|enable` operations.
|
||
Validation checks the zone as it is served: the last NS record can't be
|
||
disabled, enabling a record is checked like adding it, and a disabled
|
||
record that a CNAME now blocks gets a warning. Database schema 5.
|
||
- DNSSEC for a zone whose parent zone is in Jarl: the DNSSEC tab gives the
|
||
`jarl record add PARENT NAME DS ...` command to copy and an "Add to
|
||
PARENT" button (staged like any change), links to the parent zone, and
|
||
says when the parent has the DS record. `jarl zone dnssec status` prints
|
||
the command too, and turning signing off gives the matching `jarl record
|
||
delete`.
|
||
|
||
- CLI output for scripts: `-o id` prints just the IDs of what a command
|
||
shows or lists (`jarl zone show adbraun.lol -o id`), `-o
|
||
field=view,soa.serial` just those fields, tab separated. `-q` on `show`
|
||
and `list` prints the IDs too. With a change (`jarl record add ... -o
|
||
id`) they apply to the changed object.
|
||
|
||
- Statistics: client lists (busiest and refused clients, in the web UI,
|
||
`jarl stats top clients|refused` and `jarl stats summary`) show the host
|
||
name Jarl's records have for each address: the PTR record's target, or
|
||
else the name of an A/AAAA record. Looked up in Jarl's own data, so no
|
||
DNS queries; an address without a record shows as before.
|
||
- Zones list: a padlock after each signed zone's name, linking to its
|
||
DNSSEC tab (open and amber while signing is being turned off).
|
||
|
||
### Changed
|
||
|
||
- The web UI and API can listen on ports below 1024, such as 443:
|
||
`jarl.service` now gives jarld the `CAP_NET_BIND_SERVICE` capability
|
||
(and only that). With a unit that doesn't, jarld's error says what's
|
||
missing. See docs/configuration.md, "HTTPS on port 443".
|
||
- Records table: names are no longer greyed out when they repeat the row
|
||
above (it made those records look inactive). Records of the same name
|
||
are separated by a faint line instead, and a full line marks where the
|
||
next name starts.
|
||
|
||
### Fixed
|
||
|
||
- `jarl record list`: one long value (a DKIM key) padded every row to its
|
||
length, so on a terminal each row wrapped onto a second, blank-looking
|
||
line. Long data is now shortened to fit the terminal; `--wide` shows it
|
||
in full, as does output to a pipe or file.
|
||
|
||
## [0.2.0] - 2026-10-09
|
||
|
||
### Added
|
||
|
||
- DNSSEC signing per zone (docs/dnssec.md): `jarl zone dnssec
|
||
enable|status|check-ds|disable|wipe` and the zone page's DNSSEC tab, or
|
||
the zone's new `dnssec_policy` field in the API. named signs the zone
|
||
with BIND's `default` policy (one ECDSA P-256 key that is never replaced)
|
||
or a custom one, using inline signing, with keys in a per-view directory
|
||
under `/var/lib/bind/jarl/dnssec-keys`. Jarl shows the keys and the DS
|
||
record to give the registrar, says when it's safe to publish it, and
|
||
checks the parent's DS records (`check-ds`, which also tells named with
|
||
`rndc dnssec -checkds`). It refuses steps that would break a signed
|
||
domain: switching straight to unsigned (it goes through BIND's
|
||
`insecure` policy instead, and finishes only once named is done),
|
||
deleting a signed zone or moving it to another view, and TTLs above the
|
||
policy's `max-zone-ttl`. `wipe` starts over with a clean slate once the
|
||
parent has no DS. Each step in the web UI opens a dialog that says what it
|
||
needs first. `jarl zone delegation` now also compares the parent's DS
|
||
records with the zone's keys.
|
||
- Porkbun support (optional, docs/dnssec.md#porkbun): with an API key in
|
||
the new `porkbun:` section of jarl.yaml (and nowhere else), `jarl zone
|
||
registrar status|sync-glue|set-ns|publish-ds|remove-ds` and the zone
|
||
page's Registrar tab show and change the domain's name servers, glue
|
||
records (one per name server inside the domain, with all its IPv4 and
|
||
IPv6 addresses) and DS records at Porkbun. Every change first checks that
|
||
it won't take the domain offline (the name servers answer for the zone,
|
||
glue matches, a DS goes out only once the domain is delegated to servers
|
||
that serve the signed zone, DS records match the zone's keys before the
|
||
name servers change); `--dry-run` shows the checks and `--force` skips
|
||
them. Administrators only.
|
||
- Backups include the DNSSEC keys (private keys too), stored inside the
|
||
backup file, and `jarld restore` puts them back for named
|
||
(`bind.named_user`, default `bind`). `jarl-backup.service` has
|
||
`CAP_DAC_READ_SEARCH` to read them; `--without-keys` skips them.
|
||
- Jarl raises a zone's serial above the one named has loaded whenever it
|
||
writes a zone file whose serial isn't higher (after restoring an older
|
||
database, say), so named loads it (it refuses otherwise for a signed
|
||
zone) and secondaries pick it up.
|
||
- Two-factor login to the web UI with an authenticator app (TOTP: SHA-1,
|
||
6 digits, 30 s, as every app supports). Users turn it on under Account
|
||
(password, QR code, a code to confirm) and get 10 one-time recovery
|
||
codes. Logging in then asks for a code (or a recovery code) after the
|
||
password; each code works once. In a session with two-factor login,
|
||
creating or rotating API tokens, changing users and resetting someone's
|
||
two-factor login need a code from the last 5 minutes: the web UI asks
|
||
for one, API clients send `X-Jarl-TOTP`. API tokens and the local socket
|
||
are not affected.
|
||
- `totp.require` in jarl.yaml (`none`, `admins`, `all`) makes two-factor
|
||
login compulsory; such a user who hasn't set it up can only set it up
|
||
after logging in. `totp.issuer` names the server in authenticator apps
|
||
(default `Jarl <hostname>`).
|
||
- Resetting a user's two-factor login (lost phone): Users → Reset 2FA,
|
||
`jarl user totp reset NAME`, or on the server
|
||
`jarld user totp reset NAME` (also with jarld stopped).
|
||
- The dashboard and `jarl status` list users who must use two-factor login
|
||
but haven't set it up, and suggest disabling the first `admin` account
|
||
when it is still active next to personal administrator accounts.
|
||
`jarl user list`/`show` and the Users page show who uses it.
|
||
- API: `POST /auth/login/totp`, `/me/totp` (`setup`, `enable`, `disable`,
|
||
`recovery-codes`), `DELETE /users/{id}/totp`; database migration 4.
|
||
- `scripts/testenv.sh`: turns a fresh Debian/Ubuntu machine into a test bed.
|
||
It installs BIND and the build tools (Go from go.dev), writes a hand-made
|
||
style configuration with internal and external views, a split-horizon
|
||
zone (`example.com` or `JARL_TEST_ZONE`) and a reverse zone, starts named
|
||
and prints the steps to install Jarl and adopt the configuration
|
||
(docs/development.md).
|
||
- View colours. Give a view a colour (Views → Edit, `jarl view color`, or
|
||
`PUT /views/{id}/color`) and the zone list, the zone page and the views
|
||
list show its name in that colour. The colours are muted, and pastel in
|
||
dark mode. The default is the link colour. A colour isn't configuration,
|
||
so setting one needs no changeset or deploy.
|
||
- A "← Zones" link above a zone, back to the zone list, and "← History"
|
||
above a changeset. The zone list keeps its filter, view and type in the
|
||
address, so going back to it (with that link or the browser's back
|
||
button) restores them. A view opened from a zone or the zone list has a
|
||
"← example.com (external)" or "← Zones" link back.
|
||
|
||
### Changed
|
||
|
||
- The version moved from the header to a footer: "Powered by Jarl v0.1.0",
|
||
linking to the release on Gitea. A build that isn't of a release tag
|
||
shows its commit instead, linked to the commit and marked `-modified`
|
||
with uncommitted changes. Before, every build of a commit after a
|
||
release still showed the release's version. `GET /status` and
|
||
`/auth/whoami` report the build as `build` (docs/development.md).
|
||
- `jarld version`, `jarl --version`, `jarl status` and `jarl whoami` give
|
||
a version number only for release builds. Other builds say they are
|
||
development builds and how far past the last tag they are: `dev
|
||
ca0f148b78b6 (3 commits after v0.2.0-rc.1)`. Before, they printed the
|
||
version and the commit (`0.2.0-rc.1 (ca0f148b78b6)`), which read like a
|
||
release.
|
||
- `make deb` gives development builds their own package version after
|
||
the last tag (`0.2.0~rc.1+git3.gca0f148`, plus the build time with
|
||
uncommitted changes). Installing one over the RC or an older build is
|
||
an upgrade: no more `apt install --reinstall`. It also removes older
|
||
packages from `dist/` (docs/development.md).
|
||
- The view in the zone list, and on the zone page, links to that view,
|
||
and the views page highlights it. It used to be plain text, and the zone
|
||
page linked to the views page as a whole.
|
||
- The zone's Name servers tab shows at once and follows the check as it
|
||
happens. Each query is listed as a dig command line (`dig @192.0.2.1
|
||
example.org. SOA +norec`) with a "waiting 1.8 s" counter until it is
|
||
answered. The finished result then replaces the progress, and the queries
|
||
stay under "Queries sent". Results, the delegation check's included, are
|
||
kept for 10 minutes, so returning to the tab shows them at once; "Check
|
||
again" asks anew. A check keeps running if you leave the tab. Behind it,
|
||
`GET /zones/{zone}/serials?stream=1` streams the queries as
|
||
newline-delimited JSON (docs/api.md).
|
||
- The delegation check streams its queries the same way, every one of
|
||
them: finding the parent zone, its servers' addresses, the NS and DS
|
||
queries to each parent server and the SOA from each delegated server.
|
||
Each shows a short answer ("referral to katelyn.ns.cloudflare.com., …",
|
||
"DS 2371", "NXDOMAIN"). The serials check now lists its address lookups
|
||
as separate A and AAAA queries. `GET /zones/{zone}/delegation?stream=1`
|
||
streams them.
|
||
- Switching a zone's tabs (Records, Settings, …) no longer blanks the page
|
||
while the tab loads: the page changes once the tab is ready, and
|
||
"Loading…" appears only if a page takes a while.
|
||
- `jarl zone serials` compares the serial named serves, which for a signed
|
||
zone is the signed zone's (ahead of Jarl's).
|
||
- `jarld import` turns `dnssec-policy` in an existing configuration into
|
||
the zones' DNSSEC setting, and warns that their keys must be moved into
|
||
Jarl's key directory before adopting.
|
||
- `dnssec-policy` is no longer accepted as a zone, view or server option
|
||
(zones have a DNSSEC setting instead), nor `key-directory` as a zone
|
||
option.
|
||
- Deleting a zone also removes the journal and signed files named kept
|
||
next to it.
|
||
- Release candidates (tags like `v0.2.0-rc.1`) publish their `.deb`s to a
|
||
separate `testing` distribution of the package registry instead of
|
||
`stable`. Servers that follow `stable` can't pick up an RC with
|
||
`apt upgrade`. An RC may also take its release notes from the
|
||
`## [0.2.0]` or `## [Unreleased]` section of the CHANGELOG instead of
|
||
needing its own (docs/development.md, "Release candidates").
|
||
|
||
### Fixed
|
||
|
||
- Publishing a release candidate failed: `scripts/gitea-release.sh`
|
||
rejected its `.deb`s, whose names carry the Debian version
|
||
(`0.2.0~rc.1`) instead of `0.2.0-rc.1`.
|
||
- `jarld init -u` and the install guide recommend naming the first
|
||
administrator after yourself (one account per person).
|
||
- `jarld import` no longer warns that a zone with `allow-update { none; }`
|
||
uses dynamic updates (validation already allowed it).
|
||
- docs/cli.md gives the CLI configuration file's location on macOS
|
||
(`~/Library/Application Support/jarl/config.yaml`) and Windows
|
||
(`%AppData%\jarl\config.yaml`). It was documented only as
|
||
`~/.config/jarl/config.yaml`.
|
||
|
||
## [0.1.0] - 2026-10-09
|
||
|
||
The first version.
|
||
|
||
### Added
|
||
|
||
- Daily database backups: the `jarl-backup.timer` systemd timer (turned on
|
||
by the Debian package) runs `jarld backup`, which writes
|
||
`jarl-<UTC time>.db` into `backup.dir` (`/var/backups/jarl`), checks it
|
||
with SQLite's integrity check before giving it that name, and deletes
|
||
backups older than `backup.retention` (30d) while always keeping the
|
||
newest `backup.keep_at_least` (7). Ages come from the names, and only
|
||
files named like backups are ever deleted. Settings are in the new
|
||
`backup:` section of jarl.yaml; the schedule is the timer's. The dashboard
|
||
and `jarl status` show the newest backup, and warn when it is older than
|
||
`backup.warn_after` (2d) or the last attempt failed. `backup.stats` also
|
||
backs up the statistics database.
|
||
- `jarld restore [FILE | latest]`: lists backups, or checks one, shows what
|
||
it holds next to the current database, and after confirmation replaces
|
||
the database with it, keeping the current one as
|
||
`jarl.db.before-restore-<time>`. It refuses while jarld runs: `jarld
|
||
serve` now holds a lock (`jarl.db.lock`), which also stops two jarlds from
|
||
using the same database. docs/operations.md explains restoring.
|
||
- Warnings before API tokens expire, from 14 days ahead: on the dashboard,
|
||
in token lists ("soon", "expired"), once a day in jarld's log, and from
|
||
the `jarl` CLI on stderr whenever it uses such a token (responses carry a
|
||
`Jarl-Token-Expires` header). `jarl token list --expiring [WITHIN]` lists
|
||
them.
|
||
- `jarl zone serials ZONE` and the zone page's Name servers tab: ask the
|
||
zone's other name servers which serial they serve (a primary zone's NS
|
||
records and also-notify targets, a secondary zone's primaries, with their
|
||
ports and TSIG keys) and compare it with this server's. Exits with 2 when
|
||
one is behind or doesn't answer.
|
||
- `jarl zone delegation ZONE` and a button on the Name servers tab: for
|
||
zones on the internet, checks that the parent zone delegates the zone to
|
||
the name servers in its NS records, with matching glue, and that each of
|
||
them serves it. An undelegated (internal-only) zone gets one calm note.
|
||
Neither check runs unless asked.
|
||
- jarld sends `Server: Jarl/<version>`.
|
||
- CLI exit code 2: a check ran and found problems.
|
||
- CLI: several servers in one configuration file. Name them
|
||
(`jarl config server add ns1 URL`), pick one with `-s/--server NAME` or
|
||
`JARL_SERVER`, set a default with `jarl config use`, and group them
|
||
(`jarl config group set dns ns1 ns2`). A command aimed at a group (or
|
||
`all`) is first checked on every server as a dry run and only runs if it
|
||
succeeds everywhere, so a change one server rejects changes nothing
|
||
anywhere. Named servers always use their own token.
|
||
- Reverse zones for any network: a /32 (or /128) gets a zone that answers
|
||
for just that address, and a network between octet (or nibble)
|
||
boundaries, such as a /22, is covered by several zones (four /24s) that
|
||
are created together, from the CLI (`jarl zone create 10.0.8.0/22`) and
|
||
the web UI. Networks smaller than a /24 explain that they belong in the
|
||
enclosing /24's zone.
|
||
- docs/operations.md: setting up a secondary server with its own jarld.
|
||
- A local unix socket, `/run/jarl/jarl.sock` (`socket` in jarl.yaml).
|
||
Requests on it from root or jarld's own user need no token and act as
|
||
an administrator (the kernel's `SO_PEERCRED` says who is connecting);
|
||
anyone else must send a token. The `jarl` CLI uses it when no server is
|
||
configured, or with `-s local`, so `sudo jarl status` works on the
|
||
server with nothing set up, and losing every token or password never
|
||
locks root out. Changes are audited under the system user's name via
|
||
`socket`. The systemd unit creates `/run/jarl` (0750).
|
||
- Jarl is released under the MIT License (`LICENSE`), which is installed
|
||
with the docs and is the Debian package's `copyright` file.
|
||
- Debian packages: `make deb` builds, with only `dpkg-deb` (no root) and
|
||
for the host or any other architecture (`DEB_ARCH=arm64`), `jarl` (the
|
||
server: jarld, its units and docs) and `jarl-cli` (the `jarl` CLI, its
|
||
completion and the ACME hooks), which `jarl` depends on. `jarl-cli` has no
|
||
scripts, so it can go on any machine you manage Jarl from. Installing
|
||
`jarl` creates the `jarl` user and directories and an example
|
||
`/etc/jarl/jarl.yaml`; upgrades restart a running jarld and never touch
|
||
`/etc/jarl/jarl.yaml`; removing it stops jarld, and even a purge keeps
|
||
Jarl's database and the files named uses.
|
||
- Releases from Gitea Actions: pushing a `v<version>` tag runs
|
||
`.gitea/workflows/release.yml`, which tests, builds and publishes a Gitea
|
||
release (the CHANGELOG section as notes; the tarballs, the CLI for Linux,
|
||
macOS and Windows, the `.deb`s and `SHA256SUMS`), and uploads the `.deb`s
|
||
for amd64 and arm64 to the Gitea Debian registry, so servers install and
|
||
upgrade Jarl with apt. `make cli-dist` builds the standalone CLI files;
|
||
`scripts/gitea-release.sh` publishes. docs/development.md has the setup
|
||
(a runner and a `RELEASE_TOKEN` secret), docs/install.md the apt source.
|
||
- Bash completion for `jarl` and `jarld`, installed by `make install` and
|
||
the Debian package. Besides commands and flags, it completes server and
|
||
group names from the CLI configuration, zone, view, ACL, TSIG key and
|
||
user names from the server, and record types.
|
||
- `make dist` tarballs include the completion scripts (`completion/`).
|
||
- Token rotation: `jarl token rotate`, the Rotate button in the web UI and
|
||
`POST /api/v1/tokens/{id}/rotate` give a token a new secret while it
|
||
keeps its ID, name, user and grants. The old secret stops at once or after
|
||
a grace period of up to 7 days (`--grace`), during which jarld logs who
|
||
still uses it. A token that expires gets its lifetime again. `--save`
|
||
writes the new secret where the CLI got the old one (its configuration or
|
||
`--token @FILE`), `--write FILE` to any file, and `--current` picks the
|
||
token the CLI runs with, so a restricted token can renew itself.
|
||
`jarl token list` shows when a token was last rotated.
|
||
- An endpoint for lego's `httpreq` DNS provider
|
||
(`/api/v1/acme/httpreq/present` and `/cleanup`), so lego and Traefik can
|
||
publish DNS-01 challenges with only environment variables, without the
|
||
jarl CLI. lego's username and API token arrive as HTTP Basic auth, which
|
||
is accepted on these routes only. Off by default
|
||
(`acme.httpreq.enabled`), and HTTPS-only by default: jarld's own TLS, or a
|
||
trusted proxy that says so with `X-Forwarded-Proto` or `Forwarded`.
|
||
Plain HTTP can be allowed from loopback only (`allow_loopback_http`) or
|
||
everywhere (`require_https: false`). `GET /api/v1/acme/httpreq` shows how
|
||
jarld sees a request, to check a proxy setup. `jarl acme` and the endpoint
|
||
share their code (`internal/acme`).
|
||
- Trusted proxies may also send the standard `Forwarded: proto=https`
|
||
header. When a proxy header lists several values, the last one (added by
|
||
the nearest proxy) counts.
|
||
- lego support: `contrib/lego/jarl-lego.sh` is a program for lego's `exec`
|
||
DNS provider that publishes DNS-01 challenges through `jarl acme`,
|
||
including challenge names reached through a CNAME (docs/acme.md).
|
||
- docs/concepts.md: what each zone type does, what to enter as a new zone's
|
||
name servers (and when glue addresses are needed), how reverse zones map
|
||
to networks, and why dynamic updates give way to API tokens.
|
||
|
||
- A logo: a bearded Viking jarl in blue and white, whose crown holds a DNS
|
||
tree (a root node and its branches). The master is `artwork/logo.png`;
|
||
`scripts/logo.py` traces it into vector paths (`assets/logo.svg`, 6 KB,
|
||
and `assets/logo.js`). It is shown on the login page, links home in the
|
||
top left of the web UI, and is the favicon. The UI draws it inline, so it
|
||
appears together with the page.
|
||
- The login page shows an angular, Norse-style "JARL" wordmark drawn as
|
||
SVG paths (`assets/brand.js`) above a rule with a "DNS" node; it follows
|
||
the light and dark themes. The header beside the logo shows the same
|
||
letters, without the rule, so the UI needs no web font.
|
||
- The README opens with the login page's banner, the logo above the
|
||
wordmark (`artwork/banner.svg`, built by `scripts/banner.py` from the web
|
||
UI's own logo and wordmark, with light and dark colors).
|
||
|
||
- Web UI: wherever an address match list is entered (server, view and zone
|
||
options such as allow-query or allow-transfer, view match lists, ACL
|
||
elements) the existing ACLs, TSIG keys and built-in names are offered as
|
||
one-click suggestions, and names that don't exist are flagged as you type.
|
||
|
||
- **Query statistics** (off by default; `stats` in jarl.yaml, docs/stats.md).
|
||
When enabled, Jarl adds a loopback statistics channel and a response log to
|
||
named's configuration (merged into existing `logging` and
|
||
`statistics-channels` statements) and switches response logging with
|
||
`rndc responselog`. jarld aggregates named's counters and the log into a
|
||
separate SQLite database: per-minute, hourly and daily counters (queries,
|
||
result codes, record types, views, zones, cache hits, upstream latency,
|
||
DNSSEC) and hourly and daily top lists (names, clients, NXDOMAIN and
|
||
SERVFAIL names, refused clients), each with its own retention. Clients can
|
||
be anonymized or excluded. It is shown on a new Statistics page in the web
|
||
UI (KPI tiles, queries and failed answers over time, a weekday x hour
|
||
heatmap, top lists and breakdowns, each chart with a table view), in
|
||
`jarl stats status|summary|top|timeline|heatmap|breakdown`, and in the API
|
||
under `/api/v1/stats/`, behind the new `stats` permission. Works with the
|
||
bind9 packages of Debian 13 and Ubuntu 26.04.
|
||
- At startup jarld brings named's configuration up to date with jarl.yaml
|
||
settings that affect it (such as statistics), through the usual checked
|
||
pipeline. Files changed by hand are never overwritten.
|
||
- **jarld daemon**: REST API (`/api/v1`) and embedded web UI, with SQLite as
|
||
the source of truth for the whole BIND configuration. Runs under systemd
|
||
(`Type=notify`, sandboxed, unprivileged `jarl` user in group `bind`).
|
||
SIGHUP and certificate file changes reload TLS.
|
||
- **Configuration model**: views (ordered, match-clients/destinations,
|
||
recursive-only), ACLs (nested, negation, keys, reference tracking with
|
||
rename and delete protection), TSIG keys (kept in a separate `keys.conf`,
|
||
never in diffs), zones of every type (primary, secondary, stub,
|
||
static-stub, forward, mirror, hint, redirect, in-view), reverse zones
|
||
from a CIDR, records of every type, options at server, view, zone and
|
||
top-level scope, and defaults for new zones.
|
||
- **BIND 9.20 option catalog**: all 353 statements with grammar, allowed
|
||
scopes, value kinds, deprecation status and ARM descriptions, used for
|
||
validation and by the UI's option editor.
|
||
- **Changesets**: immediate one-operation changes by default; staging into
|
||
named changesets (`?changeset=`, `X-Jarl-Changeset`, `--changeset`),
|
||
batches (`POST /apply`, `jarl apply -f`), dry runs, previews with diffs,
|
||
and revert of applied changesets. Optimistic locking with object versions.
|
||
- **Apply pipeline**: whole-configuration validation, rendering, `named-checkconf -z`
|
||
and `named-checkzone` in a staging directory, atomic install, `rndc reconfig`
|
||
and per-zone reload, verification with `rndc zonestatus`, and automatic
|
||
rollback when named doesn't load the change. Automatic serials (date,
|
||
increment or unixtime policy, RFC 1982), bumped only when DNS data
|
||
changes. Drift detection for files changed outside Jarl.
|
||
- **Validation**: CNAME/DNAME rules, NS/MX/SRV targets, glue and
|
||
delegations, RRset TTL consistency, BIND 9.20 RRset/type limits, SPF/DMARC
|
||
sanity, ACL/key references and cycles, option syntax and scope, and
|
||
security lints (open resolver, open zone transfers, dynamic updates on
|
||
managed zones).
|
||
- **Records**: TXT entered as plain text (automatic quoting and 255-byte
|
||
splitting), relative and absolute names with detection of doubled
|
||
origins, PTR records kept in step with A/AAAA (`ptr`), RRset replace,
|
||
delete by match, zone file import (merge or replace).
|
||
- **Import and adoption**: `jarld import` reads an existing named.conf tree
|
||
and zone files (comments, `$INCLUDE`, `$GENERATE`, legacy
|
||
`master`/`slave` spellings); `jarld adopt` switches named to Jarl's
|
||
configuration with a backup and automatic fallback.
|
||
- **Authentication**: web logins with argon2id passwords, sessions
|
||
(HttpOnly, SameSite=Strict) and CSRF tokens, rate limiting per user and
|
||
IP; hashed API tokens (`jarl_...`) with optional expiry.
|
||
- **Permissions**: granular grants (resource × read/write, filtered by zone,
|
||
view, record name pattern and type), roles (`readonly`, `dns-editor`,
|
||
`operator`, `acme`), tokens restricted to a subset of their user's
|
||
rights, administrators.
|
||
- **Audit log** of logins, changes, users, tokens and secret reveals, with
|
||
user, token, IP and outcome, searchable and written to the journal.
|
||
- **Web UI**: dashboard, zones and records with inline editing and pending
|
||
markers, views, ACLs, TSIG keys, the option editor, review and apply,
|
||
history with revert, audit log, users, account and tokens, search.
|
||
Review mode by default, with an "apply immediately" switch.
|
||
- **jarl CLI**: zones, records, views, ACLs, TSIG keys, options, settings,
|
||
changesets, batches, history, users, tokens, audit, search, status, check
|
||
and resync. Table, JSON and YAML output, and documented exit codes.
|
||
- **ACME DNS-01**: `jarl acme set|unset`, certbot hooks
|
||
(`jarl acme certbot-auth|certbot-cleanup`), an acme.sh DNS API hook
|
||
(`contrib/acme.sh/dns_jarl.sh`), automatic zone and view discovery,
|
||
idempotent retries, and `--follow-cname` / `--resolver` for a dedicated
|
||
ACME zone.
|
||
- **Operations**: `jarld backup` (online), `jarld resync`, `jarld check`,
|
||
`jarld render`, local user recovery commands, audit retention.
|
||
- **Packaging**: Makefile (build, test, e2e, install, dist), systemd unit,
|
||
sysusers.d and tmpfiles.d files, example configuration
|
||
(`jarld config --example`).
|
||
- **Tests**: unit tests for every package, integration tests against the
|
||
real named 9.20 (checkers, reload and rollback, import and adopt), and an
|
||
end-to-end script (`scripts/e2e.sh`) that drives every CLI command
|
||
against a running named.
|
||
- **Documentation**: concepts, install and adoption, configuration, CLI,
|
||
API, permissions, ACME, validation, operations and development guides, and
|
||
a split-DNS guide for adding views to a server that has none.
|
||
|
||
### Changed
|
||
|
||
- Web UI: the review/live switch is a small "Review edits" / "Live edits"
|
||
button in the header, and the bar below it only appears while there are
|
||
pending changes.
|
||
- TSIG keys: the name help no longer calls `xfer-key` a domain name (any
|
||
letters, digits, hyphens and dots will do; both servers must use the same
|
||
name), and the keys page points to restricted API tokens for giving
|
||
scripts or people limited access to zones and records.
|
||
- Web UI, new zone dialog: choose **Domain** or **Reverse zone**. For a
|
||
reverse zone you give the network (10.10.0.0/24, 2001:db8:1::/48) and the
|
||
zone name is generated. Either way the dialog shows the exact zone name it
|
||
will create and explains problems before submitting: an address or URL
|
||
instead of a name, a host-like name such as www.example.com, a zone that
|
||
already exists in the view, a child or parent of an existing zone, and
|
||
networks that don't follow the octet or nibble boundaries.
|
||
- Web UI, record form: after adding a record every field is cleared except
|
||
the type, and the cursor returns to the name. A "Will add:" line shows the
|
||
record exactly as it will be stored, with warnings for unqualified names
|
||
(e.g. a CNAME to readthedocs.io without the final dot). In reverse zones
|
||
the form asks for the address, defaults to PTR, offers the host names of
|
||
A/AAAA records with that address, warns about a second PTR for an
|
||
address, and the record list shows each record's address.
|
||
- Reverse zones: a record name can be given as an address (10.10.0.20) in
|
||
the API and CLI too, and a PTR target without a final dot that contains a
|
||
dot (nas.adbraun.lol) is read as the full host name (with a warning); a
|
||
bare label is refused.
|
||
|
||
- `jarl resync` / `jarld resync` refuse to overwrite files that were changed
|
||
outside Jarl unless `--force` is given, as documented. Before, they always
|
||
overwrote them.
|
||
|
||
### Fixed
|
||
|
||
- The logo popped in after the page: it was an 880 KB SVG wrapping a
|
||
1254-pixel PNG, downloaded in full on every page load. It is now traced
|
||
into 6 KB of vector paths and drawn inline. Static files also get an ETag,
|
||
so reloads revalidate them with 304 answers instead of downloading them
|
||
again.
|
||
- `jarl token create --save` stored the token at the top level of the
|
||
CLI configuration even when a named server was selected; it now goes into
|
||
that server's entry.
|
||
- The web UI showed future times, such as token expiry dates, as "just
|
||
now"; they now read "in 29 days".
|
||
- The dashboard's "Pending changes (1 open)" counted empty changesets (and
|
||
other users'), so the Changes page then showed none. It now counts your
|
||
own changesets that contain changes, and the status API's
|
||
`open_changesets` only counts non-empty ones. In review mode, a draft
|
||
created for an edit that the server rejects is discarded instead of
|
||
being left open and empty.
|
||
- Under the shipped systemd unit (`RestrictSUIDSGID=yes`), the first zone
|
||
in a new view failed with "chmod /var/lib/bind/jarl/VIEW: operation not
|
||
permitted": setting the setgid bit on the new directory is refused there.
|
||
Jarl now falls back to the permission bits, so the directory is
|
||
group-writable for named, and only changes the mode when it needs to.
|
||
- Web UI: the "Jarl 0.1.0" brand link no longer underlines the gap between
|
||
the name and the version on hover.
|
||
- Web UI: creating a zone from the dashboard's "New zone" link no longer
|
||
opens the dialog a second time.
|
||
- Warnings noticed while parsing record data (a name read as absolute) were
|
||
lost before reaching the result; they are shown again.
|
||
|
||
- The negative-caching warning now uses the effective negative TTL, which
|
||
is the smaller of the SOA minimum and the zone's default TTL (RFC 2308).
|
||
It explains the real effect (new names can stay invisible to resolvers
|
||
that already asked) and gives the command that fixes it. Before, it
|
||
fired on the SOA minimum alone and wrongly blamed BIND's resolver cap.
|
||
- Problems printed by `jarld check` show zone names without the trailing dot.
|
||
- `jarl` now shows the warnings a change introduces when it is applied or
|
||
staged, not only on dry runs.
|
||
- `jarl check` and `jarl resync` no longer print named-checkconf's routine
|
||
"loaded serial" lines, only its errors and warnings.
|
||
- `jarl zone list` showed only primary zones: `zone create`'s default
|
||
`--type primary` leaked into the list filter through a shared variable.
|
||
Hint, secondary, forward and other zones are listed again.
|
||
- named-checkconf and named-checkzone output is classified by the tools'
|
||
exit code. Their warnings (e.g. `both "recursion no;" and "allow-recursion"
|
||
active`) were shown as errors. They are now shown as warnings in the
|
||
CLI, `jarld check` and the web UI.
|
||
- The `zone_outside_view` error says how to fix it (`--adopt-zones`).
|
||
- The end-to-end test starts named with `-f` like the Debian unit; with `-g`,
|
||
named sends all logging to stderr.
|