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
+45 -1
View File
@@ -15,8 +15,10 @@
package domains
import (
"encoding/binary"
"errors"
"fmt"
"os"
"sort"
"strconv"
"strings"
@@ -83,6 +85,9 @@ func ParseLimits(s string) Limits {
if k == "" {
continue
}
if strings.Contains(k, "@") { // a local part; lookups use the lowercased mailbox
k = strings.ToLower(k)
}
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
if err != nil {
continue
@@ -173,11 +178,23 @@ func Open(path string) (*DB, error) { return OpenWithCache(path, DefaultMaxCache
// OpenWithCache opens the CDB at path and counts its records (a few
// milliseconds for ~100k records; the count feeds the cdb_domains gauge and
// the reload log). maxCached bounds the parsed-record cache; 0 disables it.
func OpenWithCache(path string, maxCached int) (*DB, error) {
//
// A corrupt file is an error, never a panic: reloads run on a goroutine with
// no recover, and the cdb library panics on record lengths that overflow.
func OpenWithCache(path string, maxCached int) (db *DB, err error) {
if err := checkTables(path); err != nil {
return nil, err
}
c, err := cdb.Open(path)
if err != nil {
return nil, fmt.Errorf("open domains db %s: %w", path, err)
}
defer func() {
if p := recover(); p != nil {
c.Close()
db, err = nil, fmt.Errorf("read domains db %s: corrupt file: %v", path, p)
}
}()
n := 0
it := c.Iter()
for it.Next() {
@@ -193,6 +210,33 @@ func OpenWithCache(path string, maxCached int) (*DB, error) {
return &DB{path: path, db: c, count: n, maxCached: maxCached}, nil
}
// checkTables verifies that every hash table named in the CDB header lies
// inside the file. Walking the records never reads these tables, so a file cut
// off inside them would load and then fail lookups.
func checkTables(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open domains db %s: %w", path, err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return fmt.Errorf("open domains db %s: %w", path, err)
}
header := make([]byte, 256*8)
if _, err := f.ReadAt(header, 0); err != nil {
return fmt.Errorf("read domains db %s: header: %w", path, err)
}
for i := 0; i < 256; i++ {
offset := int64(binary.LittleEndian.Uint32(header[i*8:]))
slots := int64(binary.LittleEndian.Uint32(header[i*8+4:]))
if offset+8*slots > fi.Size() {
return fmt.Errorf("read domains db %s: hash table %d ends past the end of the file", path, i)
}
}
return nil
}
// Lookup returns the record for domain (trimmed and lowercased first), or
// ErrNotFound when the domain is not hosted here. The returned *Domain may
// be shared with other callers and must be treated as read-only.