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:
co-authored by
Claude Opus 5.5
parent
c43ff17627
commit
54f9626121
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user