Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,253 @@ Create a passphrase-sealed application backup before upgrading and keep
mixed-version cluster windows short. Cross-version restore and downgrade
compatibility are not yet a published contract.

## [1.7.0] - 2026-10-13

Sable 1.7.0 adds per-device blocking. Rule sets give some devices their own
block lists, domains, and blocked apps, a hold blocks everything for one
device for a while, and schedules block everything or chosen apps at set
times each week, such as a bedtime. Block lists now block what their authors
meant, and DNSSEC stops failing correctly signed answers that broke reverse
lookups and the OISD lists. Kubernetes ExternalDNS can manage the override
records on Forwarder zones. Sable starts faster, uses less memory per query,
and can no longer be tied up by a stuck client or a lookup that hits a bug.
The console looks the same on every page, with pill buttons, faster dialogs,
and better behavior on phones and in Safari.

### Upgrading from 1.6.x

- `blocking.bypass_clients` moves into a rule set called `No Blocking` with
blocking turned off, one `[[clients]]` entry per address or network, the
first time 1.7.0 loads the config. The key is dropped on the next save, and
the **Bypass Clients** card leaves **Settings**. An address that already
had a rule set moves to `No Blocking`, since a bypass used to win.
- Devices (`[[clients]]`), rule sets, holds, and schedules replicate to every
node, and each node compiles its own block lists. Upgrade every node in a
cluster together, and don't add rule sets, holds, or schedules until they
all run 1.7.0. Until then, nodes on different versions can answer some
names differently.
- Adblock-style lists such as EasyList, EasyPrivacy, and AdGuard DNS Filter
block fewer domains after the upgrade. Rules with a path, a wildcard, a
regular expression, or a modifier other than `$important` used to be cut
down to their host, so `||google.com/adsense/…` blocked all of google.com.
They're now skipped and counted as unsupported rules. Sites those rules
broke resolve again.
- `@@||host^` exception rules now unblock that host and its subdomains on
every list. Your own blocked domains and `$important` rules still win, and
your allowed domains are still checked first.
- Fanboy Social and Fanboy Annoyances are no longer offered in the block list
catalog, because almost none of their rules apply to DNS. An existing
subscription keeps working as a custom list; remove it if you no longer
want it.
- Allowed and blocked domains are now exclusive. Adding a domain to one list
removes it from the other, whether you use the console, MCP, the query
log's allow and block actions, or a file import.
- The MCP `lookup` tool now needs `zones.read`. A token that only has
blocking or settings access gets "this token needs zones.read to use
lookup". Groups made by the MCP setup in **Integrations** already have it.
For a group you built yourself, add `zones.read` to its API access.
- New MCP tools: `list_schedules` is on by default, and `block_device`,
`unblock_device`, and `override_schedule` are off by default. An install
that already picked its MCP tools (`[mcp] tools`) gets none of them until
you turn them on under **Integrations**.
- A TSIG-signed zone transfer, NOTIFY, or dynamic update sent over
DNS-over-HTTPS is now refused, because Sable never checked the signature on
that path. Send them over UDP, TCP, or DoT, where it does.
- The first start upgrades the database once and records its schema version,
so later starts skip that work. On PostgreSQL, nodes that share a database
take turns, and the query log indexes are built without blocking writes, so
a large log can make that first start slow.

### Per-device blocking

- Add rule sets on a new **Blocked → Rule Sets** tab. Each rule set picks its
own block lists and keeps its own blocked and allowed domains, which win
over everyone's. Devices without a rule set use the **Default** rules,
whose lists `blocking.default_lists` can narrow.
- Open a rule set to change everything about it in one panel: its name, its
devices, addresses, and networks, its block lists, its own blocked and
allowed domains, and a **Blocking** switch that turns it off for its
devices. Changes save as you make them. **Add Rule Set** asks only for a
name, then opens the panel.
- Put a device in a rule set by its hardware address. It applies at every
address Sable ties to that device, from the neighbor table, UniFi, or the
cluster lead, without Sable being the DHCP server. **Add Device** and
**Check a Domain** pick devices from a searchable list with each device's
icon and address.
- Block apps for a rule set. **Choose Apps** opens a grid of app logos by
category with a search, and each app blocks every domain Sable knows it
uses. The grid leaves out device platforms, smart home, cameras, and work
tools so nobody breaks a device by accident.
- Block everything for one device for 30 minutes, an hour, until a time, or
until you turn it off, from its **Blocking** section on **Insights →
Devices**. A live countdown, **Add 30 Minutes**, and **End Now** sit
beside it. A hold outlasts a global pause, and the device's allowed
domains still work.
- Add schedules to a rule set. Pick the days, start and end times, and
whether it blocks everything or chosen apps. An end before the start runs
into the next morning, and times follow daylight saving time. A schedule
that blocks everything works like a hold.
- Change one night without editing the schedule. **Skip**, **Delay 30
Minutes**, and **End Now** adjust the current or next window, and
**Resume** undoes them. A device's Insights panel lists its schedules,
whether each is on, and when it next starts or ends.
- Show which rule set a device uses, and why, on its Insights panel, with a
picker to give it another.
- Block or allow a query's domain for its device's rule set from the query
log. **Block** and **Allow** become split buttons such as **Block for
Warehouse**, with **Everyone** on the arrow, and remember which you picked
last.
- Name the rule set, app, hold, or schedule behind a block in the query
explanation, such as "Matched tiktokcdn.com, one of TikTok's domains,
blocked by the Warehouse rule set". **Check a Domain** and **Why is this
blocked?** check for a particular device.

### Block lists

- Apply only the host rules from adblock-style lists: `||host^`, optionally
followed by `|` or `$important`. Paths, wildcards, regular expressions,
cosmetic rules, address rules, and other modifiers are skipped, as AdGuard
Home, Technitium, and Pi-hole skip them.
- Stop treating `$badfilter` rules as blocks. AdGuard DNS Filter's
`||pl.ua^$badfilter` blocked the whole `pl.ua` suffix.
- Honor `@@||host^` exception rules across all lists. The query log and
**Check a Domain** name the list whose exception let a domain through.
- Show a list's exceptions and unsupported rules in its details, and keep
**Invalid lines** for lines that are actually malformed.
- Click anywhere on a block list or rule set row to open it. Rows end in a
chevron.

### DNSSEC

- Accept correctly signed answers that Sable failed as Bogus (EDE 6):
- Names under an empty non-terminal, a name with children but no records of
its own (RFC 4035 sections 3.1.3.2 and 5.4). Reverse lookups in ARIN,
APNIC, and LACNIC space, such as `254.55.207.192.in-addr.arpa`, failed
this way.
- Wildcard answers proved only by the record covering the next closer name
(RFC 5155 sections 7.2.6 and 8.8, RFC 4035 section 5.3.4). Names such as
`big.oisd.nl` failed this way, and with them the OISD block list refresh.
- Stop accepting proofs that don't prove anything: a parent zone's NSEC at a
delegation or DNAME offered as proof about the child zone, and an NSEC3
that matches the name or uses an unknown hash algorithm offered as proof the
name doesn't exist.
- Keep serve-stale, prefetch, and the TTL limits after a trust anchor
rollover. The cache came back with default settings.
- Stop a prefetch from replacing a validated cached answer with an
unvalidated one when local validation is off.

### DNS

- Accept RFC 2136 dynamic updates on Forwarder zones, so Kubernetes
ExternalDNS and similar tools can manage a Forwarder zone's local override
records. Secondary Forwarder zones still refuse them, and no update can add
or remove forwarding routes.
- Accept the "delete this RRset" and "RRset exists" records of an RFC 2136
update. Sable answered those updates with FORMERR.
- Answer SERVFAIL when a lookup hits a bug, and count it in
`sable_dns_panics_total`. The query used to die, and the same question from
other devices could hang until they gave up.
- Cap open connections at 1,024 across TCP, DoT, and DoH, and at 256 for
DNS-over-QUIC, so a client that opens connections and never closes them
can't use up memory.
- Check DoH and DoQ requests the same way as UDP, TCP, and DoT, and answer a
malformed one with the same DNS reply.
- Stop sending a deleted zone's records to a secondary that asks for an
incremental transfer after the zone is deleted or re-created.
- Apply configuration reloads and zone changes one at a time, so a reload
can't bring back zones a zone change had just replaced.
- Match records by lowercase name and value, ignoring TTL, in both the
console and MCP, so an edit or delete through MCP picks the same record the
console would.

### Insights

- Hide a finding straight from its row in **Worth a Look**. A round icon
beside **Details** opens the **For a Day**, **For a Week**, and **That's
Normal** menu without opening the drawer.
- Read all-time and open-ended ranges from the rollups instead of the whole
query log, and share dashboard data between open dashboards instead of
re-reading it on every poll.

### Logs

- Prune old queries in small batches. A large retention cut or a long outage
made one huge delete that could time out and start over every hour, drop new
queries while it ran, and hold the SQLite write lock the whole time.
- Stream the CSV export without skipping or repeating rows that arrive during
the download. A failure partway through fails the download instead of
saving a short file.
- Match `_` and `%` literally in query log search.

### Performance

- Start every `sable` command about 70 ms faster with 16 MiB less allocated,
and use about 3 MiB less memory when idle.
- Allocate less per query, on cache hits and DoH responses in particular.
- Load a zone's history when you open it instead of for every zone in the
list, and redraw only the zone you changed after a record edit.

### Console

- Draw every page from one set of buttons, dialogs, tabs, tables, menus, and
badges. Status badges use one green and one red, file sizes use binary
units everywhere, and every table heading is announced to screen readers as
a column heading.
- Round the console into one shape language. Buttons, segmented controls,
the selected sidebar item, and the account button are pills, and icon-only
buttons and avatars are circles. Tab bars that wrap to two rows on a phone
use rounded rectangles.
- Open the command palette, dialogs, and detail sheets faster.
- Open the navigation on a phone by dragging right from anywhere on the page.
Start a little in from the edge, since iOS Safari keeps the edge for its
back gesture.
- Keep the page behind an open sheet or dialog still on iPhone and iPad, and
keep your place on the page when a dialog closes.
- Scroll a drawer in Safari and on iPhone while the pointer or your finger
is over one of its lists.
- Stop drawing a focus ring on the current page's link when you tap open the
phone menu.
- Ask "Delete this record?" with the record's type and name before deleting
a record.
- Show a linked identity's last sign-in in your time setting instead of UTC.
- List the cards on **Integrations** alphabetically.

### Dynamic DNS

- Summarize a provider outage as one short line per failure, such as
"Cloudflare had a temporary problem (502 Bad Gateway). Sable will try
again." The raw response is still under **View provider details**.
- Stop sending requests to a provider for the rest of a run after its first
rate limit or server error, and wait as long as its `Retry-After` asks, up
to 30 minutes. Other providers still update.

### MCP

- Give `check_domain` an optional `device`, and return the `rule_set` and
`app` that decided.
- Add `block_device` and `unblock_device` to set and end holds,
`list_schedules` to list each rule set's schedules, and
`override_schedule` to skip, delay, end, or resume a window.

### Monitoring

- Add the standard Prometheus process and Go runtime metrics to `/metrics`,
such as `process_resident_memory_bytes`, `process_cpu_seconds_total`,
`process_open_fds`, `go_goroutines`, and `go_gc_duration_seconds`.
- Add memory, CPU, and garbage collector figures to the MCP `get_stats`
result.

### Updates

- Check for updates through Sable's own resolver, falling back to the host's
only when Sable has no answer. On hosts whose resolver drops queries, such
as some Starlink routers, the check timed out.

### Security

- Build with Go 1.27.2 and golang.org/x/net 0.60.0, which fix advisories in
`net/http`, its HTTP/2 code, `net/textproto`, and `crypto/tls`.

## [1.7.0-beta.17] - 2026-10-13

Sable 1.7.0-beta.17 lets you scroll a drawer in Safari while the pointer or
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/technitium-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Build Sable alongside your current DNS service, move one test zone, and verify it before moving clients. This guide covers migration to Sable: zone-file import, a one-time AXFR snapshot, synchronized Secondary staging, or bulk import from a catalog. The older-version cutover procedure is retained below.

> [!NOTE]
> **Version availability:** This guide covers Sable 1.6.1. Authoritative conversion and Import from Catalog arrived in 1.2.0; forwarder import, synchronization, and conversion require 1.3.2. Users on 1.1.0 and earlier can upgrade or follow the older export/remove/import procedure below.
> **Version availability:** This guide covers Sable 1.7.0. Authoritative conversion and Import from Catalog arrived in 1.2.0; forwarder import, synchronization, and conversion require 1.3.2. Users on 1.1.0 and earlier can upgrade or follow the older export/remove/import procedure below.

## Choose a migration path

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Start with one Sable server and one test device. Get your first DNS answer, then
- **Configuration and operations references** describe exact behavior when you need to go deeper.

> [!NOTE]
These guides cover Sable 1.6.1. [Download the release](https://github.com/drudge/sable/releases/tag/v1.6.1) or read the [release notes](../CHANGELOG.md).
These guides cover Sable 1.7.0. [Download the release](https://github.com/drudge/sable/releases/tag/v1.7.0) or read the [release notes](../CHANGELOG.md).

## Migrating from Technitium

Expand Down
4 changes: 2 additions & 2 deletions docs/navigation.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "1.6.1",
"versionLabel": "1.6.1",
"version": "1.7.0",
"versionLabel": "1.7.0",
"navigationLabels": {
"getting-started": "First server",
"guides/technitium-migration": "From Technitium",
Expand Down
Loading