Compare commits

..
Author SHA1 Message Date
abrown38andClaude Opus 5.5 6f56eefae2 Release 0.1.1-rc.13
release / release (push) Successful in 1m5s
Housecarl 0.1.1's thirteenth release candidate. Since rc.12: quota usage
is saved after every change. make test and make e2e (304 checks) pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:04:04 +01:00
abrown38andClaude Opus 5.5 aaf0ac5206 Quotas: save their usage after every change
The server hooks the engine's OnChange to save what the quotas used, so
a reboot soon after a change that counts a quota differently doesn't
bring back its usage counted the old way (it was saved every five
minutes and on stop only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:04:04 +01:00
7 changed files with 35 additions and 7 deletions

No files matched your search

+3
View File
@@ -96,6 +96,9 @@ All notable changes to Housecarl are documented here. The format follows
the old entries behind. Now a total starts at the addresses' sum, the old entries behind. Now a total starts at the addresses' sum,
wider groups add up their addresses, and what can't be carried starts wider groups add up their addresses, and what can't be carried starts
again with a warning (`quota_restarts`) on the change. again with a warning (`quota_restarts`) on the change.
- What the quotas used is saved after every change, not only every five
minutes: a reboot soon after a change that counts a quota differently
could bring back its usage counted the old way.
- Statistics' Quotas card linked every entry to the Rules page, not to - Statistics' Quotas card linked every entry to the Rules page, not to
its rule; each now opens its rule, named. Per-address quotas were its rule; each now opens its rule, named. Per-address quotas were
missing from it (and from the `quota:rN` series): they count their missing from it (and from the `quota:rN` series): they count their
+1 -1
View File
@@ -123,7 +123,7 @@ The web UI is on `http://127.0.0.1:8070/` (see `listen` in
## Status ## Status
Housecarl 0.1.1 is in testing (0.1.1-rc.12): zones, address sets and Housecarl 0.1.1 is in testing (0.1.1-rc.13): zones, address sets and
services with ordered rules, IPv4 and IPv6, port forwards with hairpin NAT services with ordered rules, IPv4 and IPv6, port forwards with hairpin NAT
and IPv6 pinholes, source NAT and redirects, built-ins, probation for and IPv6 pinholes, source NAT and redirects, built-ins, probation for
network changes (confirmed by the client that made them), the ruleset network changes (confirmed by the client that made them), the ruleset
+1 -1
View File
@@ -1 +1 @@
0.1.1-rc.12 0.1.1-rc.13
+3 -3
View File
@@ -164,9 +164,9 @@ start again. A disabled rule keeps its count for when it is enabled.
resets; `hc rule reset-quota ID` starts one again (`--address` one device's, resets; `hc rule reset-quota ID` starts one again (`--address` one device's,
`--give 5gbytes` a top-up until the next reset). housecarld resets `--give 5gbytes` a top-up until the next reset). housecarld resets
quotas at midnight when their period ends (late, if it was stopped then), quotas at midnight when their period ends (late, if it was stopped then),
and saves their usage every five minutes and when it stops; after a and saves their usage every five minutes, after every change and when
reboot it puts it back. A reboot without a clean stop loses at most the it stops; after a reboot it puts it back. A reboot without a clean stop
last five minutes' count. loses at most the last five minutes' count.
## The log ## The log
+2 -2
View File
@@ -209,8 +209,8 @@ Statistics**.
nftables counts a quota but never resets it, and forgets it when its nftables counts a quota but never resets it, and forgets it when its
table is replaced: Housecarl carries what each quota used across every table is replaced: Housecarl carries what each quota used across every
change and restart, saves it every five minutes (and when housecarld change and restart, saves it every five minutes (and after each change,
stops) for after a reboot, and resets quotas when their period ends. and when housecarld stops) for after a reboot, and resets quotas when their period ends.
A per-address quota counts an address: a device that gets a new one A per-address quota counts an address: a device that gets a new one
starts again. With Steward's reservations IPv4 addresses stay; IPv6 is starts again. With Steward's reservations IPv4 addresses stay; IPv6 is
grouped by /64 (below). grouped by /64 (below).
+10
View File
@@ -141,6 +141,16 @@ func New(cfg Config, eng *engine.Engine, fw Firewall, ifaces Interfaces, log *sl
s := &Server{cfg: cfg, eng: eng, fw: fw, ifaces: ifaces, store: eng.Store(), log: log, mux: http.NewServeMux(), limiter: newRateLimiter(), s := &Server{cfg: cfg, eng: eng, fw: fw, ifaces: ifaces, store: eng.Store(), log: log, mux: http.NewServeMux(), limiter: newRateLimiter(),
web: web, started: time.Now(), logs: newLogRing(2000), nowFn: time.Now, unitLinks: nftablesUnitLinks} web: web, started: time.Now(), logs: newLogRing(2000), nowFn: time.Now, unitLinks: nftablesUnitLinks}
s.routes() s.routes()
// What the quotas used is saved after every change too, not only every
// five minutes: a change that counts a quota differently converts its
// usage, and a restart must not bring back the old kind.
prev := eng.OnChange
eng.OnChange = func(st *model.State) {
if prev != nil {
prev(st)
}
s.SaveQuotaUsage(context.Background())
}
return s return s
} }
+15
View File
@@ -792,6 +792,21 @@ func TestQuotaResets(t *testing.T) {
} }
} }
// TestQuotaUsageSavedOnChange: a change saves what the quotas used at
// once (a restart must not bring back usage counted the old way).
func TestQuotaUsageSavedOnChange(t *testing.T) {
e := newEnv(t)
e.call(e.token, "POST", "/zones", model.Zone{Name: "lan", Interfaces: []string{"eth1"}}, nil)
e.fw.mu.Lock()
e.fw.quotas = nft.QuotaUsage{Quotas: map[string]int64{"quota_r7": 4242}}
e.fw.mu.Unlock()
e.call(e.token, "POST", "/zones", model.Zone{Name: "dmz", Interfaces: []string{"eth2"}}, nil)
v, err := e.eng.Store().GetMeta(context.Background(), QuotaUsageKey)
if err != nil || !strings.Contains(v, `"quota_r7":4242`) {
t.Errorf("saved usage %q %v", v, err)
}
}
// TestAutoConfirm: the client that made a change confirms it by itself: // TestAutoConfirm: the client that made a change confirms it by itself:
// with its token, from the same address, over a new connection. Nobody // with its token, from the same address, over a new connection. Nobody
// else can, an older token can't, and with manual_confirm there is none. // else can, an older token can't, and with manual_confirm there is none.