Add 80% limit warning, stop repeat notifications, fix review findings

Notifications:
- Warn once when a counter reaches warn_percent of its limit, through
  separate warn_*_url endpoints.
- Write ntf/ markers so a lost Redis reply cannot re-send a breach.
- Retry only when the request never left or the web app answered 429/5xx.

Fixes:
- Retry transactions Redis rejected; treat pool timeouts as definite.
- Install the signal handler before startup work.
- Stop logging the login suffix; redact notify URL credentials.
- Reject corrupt or empty CDBs, truncated hash tables included.
- Require a Sentinel password, refuse example placeholders, and default
  admin_addr to localhost.
- Normalise /counters domains, count on the arrival day, log rejections,
  hide internals in errors and exit 1 on a failed drain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dimitrije Dimitrijevic
2026-11-09 09:01:56 +01:00
co-authored by Claude Opus 5.5
parent c43ff17627
commit 54f9626121
29 changed files with 1673 additions and 245 deletions
+58
View File
@@ -1,7 +1,9 @@
package domains
import (
"encoding/binary"
"errors"
"os"
"path/filepath"
"strings"
"testing"
@@ -387,3 +389,59 @@ func TestMailbox(t *testing.T) {
}
}
}
func TestParseLimits_OverrideKeysLowercased(t *testing.T) {
l := ParseLimits("A/o=20,Info@/o=5,O/o=9")
if n, ok := l.Limit(AddressLevel, FlowOut, "info"); !ok || n != 5 {
t.Errorf("override for info = %d, %v; want 5", n, ok)
}
if n, ok := l.Limit(OrganizationLevel, FlowOut, ""); !ok || n != 9 {
t.Errorf("O/o = %d, %v; level keys must keep their case", n, ok)
}
}
// A file cut off inside its hash tables passes a walk over the records, but
// lookups would then fail: it must be refused at open.
func TestOpen_RejectsTruncatedHashTables(t *testing.T) {
path := filepath.Join(t.TempDir(), "t.cdb")
w, err := cdb.Create(path)
if err != nil {
t.Fatal(err)
}
for _, k := range []string{"a.example", "b.example", "c.example", "d.example"} {
if err := w.Put([]byte(k), []byte("1|A/o=1")); err != nil {
t.Fatal(err)
}
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
fi, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if err := os.Truncate(path, fi.Size()-8); err != nil {
t.Fatal(err)
}
if db, err := Open(path); err == nil {
db.Close()
t.Fatal("truncated CDB was accepted")
}
}
// Lengths that overflow uint32 make the cdb library panic; a bad file must be
// an error, because reloads run on a goroutine with no recover.
func TestOpen_CorruptLengthsAreAnError(t *testing.T) {
buf := make([]byte, 2048+32)
binary.LittleEndian.PutUint32(buf[0:], 2048+32) // table 0 starts after one bogus record
binary.LittleEndian.PutUint32(buf[2048:], 0xFFFFFFFF)
binary.LittleEndian.PutUint32(buf[2052:], 2)
path := filepath.Join(t.TempDir(), "bad.cdb")
if err := os.WriteFile(path, buf, 0o600); err != nil {
t.Fatal(err)
}
if db, err := Open(path); err == nil {
db.Close()
t.Fatal("corrupt CDB was accepted")
}
}