Skip to content

Block everything for a device for a while - #413

Merged
drudge merged 1 commit into
mainfrom
claude/per-device-blocking-0lzpfb
Oct 8, 2026
Merged

drudge merged 1 commit into
mainfrom
claude/per-device-blocking-0lzpfb

Conversation

@drudge

@drudge drudge commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Before: the only way to cut a device off was to pause blocking for everyone or block domains for everyone. There was no "no internet until homework's done" for one device.

After: a hold blocks everything for one device, for some minutes, until a time, or until it's removed. The device still reaches its allowed domains (its rule set's and the global ones) and the names Sable answers itself from zones and host overrides, so the console and captive checks keep working. A hold wins over the device's rule set and bypass_clients, and keeps blocking while blocking is paused. Holds live in config, so they replicate to every node and each one shows in the Change Center. The query log explains a held query as "Everything blocked for this device".

[[blocking.holds]]
mac = "da:a1:19:00:00:01"
until = 2026-10-08T20:00:00-04:00

This is PR 3 of #240. The countdown, Add 30 Minutes and End Now buttons come with the console PR, on top of the hold service added here. For now holds can be set through config or the new MCP tools block_device and unblock_device, which start off like the other tools that change things a lot.

How

  • One client lookup for rule sets and holds. dnsserver.clientTable[V] finds a client by exact address, then by the hardware address behind it, then by the most specific network. Rule sets use clientTable[int], holds use clientTable[int64] (end time in Unix nanoseconds, 0 for no end).
  • The device address table now maps address → hardware address instead of address → rule set name. Rule sets and holds both name MAC devices in the runtime config, and the worker (renamed deviceAddressTable) tracks every MAC that a rule set or a hold names. A rule set rename no longer has to reach the table.
  • Expiry needs no timer. An ended hold just stops matching at query time. config.SetHold and config.EndHold drop ended holds whenever a hold changes, so they don't pile up.
  • holdService (in internal/web) sets and ends holds through policyService's blocking.write check, replica refusal, config transaction and audit. With Insights off it refuses a hold by hardware address, matching Blocking: per-device rules and "Block everything" for a device #240's "IP/CIDR only" decision. An IP address Sable has tied to hardware is held by hardware, so the hold follows the device to its IPv6 addresses.
  • New decision querylog.PolicyHeld. It answers with the normal blocked response and counts as blocked.

Benchmarks

BenchmarkPolicyDecision, 100k domains, 300k iterations, 3 runs on a noisy shared container. The rule-set cases now also have two holds on other clients:

main ns/op this PR ns/op allocs
NoRuleSets ~150 ~127 0
OutsideRuleSets ~131 ~167 0
InRuleSet ~238 ~293 0
LearnedDevice ~234–406 ~208–302 0

With any hold configured, a query pays one more map lookup, roughly 35 to 50 ns. A query without any holds pays nothing extra. TestPolicyDecisionDoesNotAllocateWithHolds asserts 0 allocations for held, ended, network and learned clients.

Checks

go tool mage verify passes, gofmt -l . is clean, and the new tests pass under -race.

Refs #240


Generated by Claude Code

A hold blocks every domain for one device except its allowed domains and
the names Sable answers itself, for some minutes, until a time, or until it
is removed. Holds live in [[blocking.holds]], so they replicate and show in
the Change Center, and the query log explains held queries.

The device address table now ties addresses to hardware addresses rather
than rule set names, so rule sets and holds share one client lookup. The MCP
tools block_device and unblock_device set and end holds; both start off.
@drudge
drudge marked this pull request as ready for review October 8, 2026 21:10
@drudge
drudge merged commit 13b2f40 into main Oct 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant