Bug bash fixes: special-file reads, per-client lockout, prefs validation, security headers, kernel-thread guard, login feedback, error text; plus journal-quiet sudoers + journal groups, upload overwrite guard, machines pattern, dnf freshness, cron/services speed, clean version
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
57c58ebffb
commit
27f9665606
+64
-11
@@ -152,39 +152,92 @@ func (s *Sessions) Delete(tok string) {
|
||||
|
||||
// Limiter is a single global backoff: after 5 failures, each further failure
|
||||
// doubles a lockout starting at 30s, capped at 10 minutes. Single-user box.
|
||||
// Limiter locks out a client after repeated failed logins, per client address, with a
|
||||
// global backstop so a botnet can't burn CPU on argon2 indefinitely. A lockout for one
|
||||
// address never blocks another.
|
||||
type Limiter struct {
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
clients map[string]*bucket
|
||||
global bucket
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
failures int
|
||||
lockUntil time.Time
|
||||
last time.Time
|
||||
}
|
||||
|
||||
var ErrLocked = errors.New("too many failed logins")
|
||||
|
||||
func (l *Limiter) Allow() error {
|
||||
const (
|
||||
perClientLock = 5 // failures before a client is locked
|
||||
globalLock = 100 // failures (all clients) in the window before everyone waits a minute
|
||||
globalWindow = 5 * time.Minute
|
||||
)
|
||||
|
||||
func (l *Limiter) get(key string) *bucket {
|
||||
if l.clients == nil {
|
||||
l.clients = map[string]*bucket{}
|
||||
}
|
||||
b, ok := l.clients[key]
|
||||
if !ok {
|
||||
b = &bucket{}
|
||||
l.clients[key] = b
|
||||
}
|
||||
// forget clients that have been quiet for an hour
|
||||
if len(l.clients) > 256 {
|
||||
for k, x := range l.clients {
|
||||
if time.Since(x.last) > time.Hour && time.Now().After(x.lockUntil) {
|
||||
delete(l.clients, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (l *Limiter) Allow(key string) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if time.Now().Before(l.lockUntil) {
|
||||
now := time.Now()
|
||||
if now.Before(l.global.lockUntil) {
|
||||
return ErrLocked
|
||||
}
|
||||
if now.Sub(l.global.last) > globalWindow {
|
||||
l.global.failures = 0
|
||||
}
|
||||
if b := l.get(key); now.Before(b.lockUntil) {
|
||||
return ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Limiter) Fail() {
|
||||
func (l *Limiter) Fail(key string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.failures++
|
||||
if l.failures >= 5 {
|
||||
d := 30 * time.Second << uint(l.failures-5)
|
||||
now := time.Now()
|
||||
b := l.get(key)
|
||||
b.failures++
|
||||
b.last = now
|
||||
if b.failures >= perClientLock {
|
||||
d := 30 * time.Second << uint(b.failures-perClientLock)
|
||||
if d > 10*time.Minute {
|
||||
d = 10 * time.Minute
|
||||
}
|
||||
l.lockUntil = time.Now().Add(d)
|
||||
b.lockUntil = now.Add(d)
|
||||
}
|
||||
if now.Sub(l.global.last) > globalWindow {
|
||||
l.global.failures = 0
|
||||
}
|
||||
l.global.failures++
|
||||
l.global.last = now
|
||||
if l.global.failures >= globalLock {
|
||||
l.global.lockUntil = now.Add(time.Minute)
|
||||
l.global.failures = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Limiter) Reset() {
|
||||
func (l *Limiter) Reset(key string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.failures = 0
|
||||
l.lockUntil = time.Time{}
|
||||
delete(l.clients, key)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user