diff --git a/CHANGELOG.md b/CHANGELOG.md index 8561313..292219f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, wider groups add up their addresses, and what can't be carried starts 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 its rule; each now opens its rule, named. Per-address quotas were missing from it (and from the `quota:rN` series): they count their diff --git a/docs/operations.md b/docs/operations.md index f3c91d2..6dbeeb5 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -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, `--give 5gbytes` a top-up until the next reset). housecarld resets 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 -reboot it puts it back. A reboot without a clean stop loses at most the -last five minutes' count. +and saves their usage every five minutes, after every change and when +it stops; after a reboot it puts it back. A reboot without a clean stop +loses at most the last five minutes' count. ## The log diff --git a/docs/rules.md b/docs/rules.md index 15f52a9..46b7e49 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -209,8 +209,8 @@ Statistics**. nftables counts a quota but never resets it, and forgets it when its table is replaced: Housecarl carries what each quota used across every -change and restart, saves it every five minutes (and when housecarld -stops) for after a reboot, and resets quotas when their period ends. +change and restart, saves it every five minutes (and after each change, +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 starts again. With Steward's reservations IPv4 addresses stay; IPv6 is grouped by /64 (below). diff --git a/internal/server/server.go b/internal/server/server.go index 9229813..58ccfb0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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(), web: web, started: time.Now(), logs: newLogRing(2000), nowFn: time.Now, unitLinks: nftablesUnitLinks} 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 } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 9a5e1a3..650edf2 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -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: // 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.