login_rate_limit.go 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. package server
  2. import (
  3. "sync"
  4. "time"
  5. )
  6. // loginRateLimiter blunts online brute-force attacks by temporarily locking a
  7. // key (client IP + username) after too many consecutive failed logins.
  8. type loginRateLimiter struct {
  9. mu sync.Mutex
  10. attempts map[string]*loginAttempt
  11. maxFailures int
  12. window time.Duration
  13. lockout time.Duration
  14. now func() time.Time
  15. }
  16. type loginAttempt struct {
  17. failures int
  18. firstFail time.Time
  19. lockedUntil time.Time
  20. }
  21. func newLoginRateLimiter() *loginRateLimiter {
  22. return &loginRateLimiter{
  23. attempts: make(map[string]*loginAttempt),
  24. maxFailures: 5,
  25. window: 10 * time.Minute,
  26. lockout: 10 * time.Minute,
  27. now: time.Now,
  28. }
  29. }
  30. // checkLocked reports whether the key is currently locked and for how much longer.
  31. func (l *loginRateLimiter) checkLocked(key string) (time.Duration, bool) {
  32. l.mu.Lock()
  33. defer l.mu.Unlock()
  34. attempt, ok := l.attempts[key]
  35. if !ok {
  36. return 0, false
  37. }
  38. now := l.now()
  39. if now.Before(attempt.lockedUntil) {
  40. return attempt.lockedUntil.Sub(now), true
  41. }
  42. return 0, false
  43. }
  44. // recordFailure registers a failed attempt and locks the key once the failure
  45. // threshold is reached within the window. It returns the lockout duration when
  46. // a lock is newly applied.
  47. func (l *loginRateLimiter) recordFailure(key string) (time.Duration, bool) {
  48. l.mu.Lock()
  49. defer l.mu.Unlock()
  50. now := l.now()
  51. attempt, ok := l.attempts[key]
  52. if !ok || now.Sub(attempt.firstFail) > l.window {
  53. attempt = &loginAttempt{firstFail: now}
  54. l.attempts[key] = attempt
  55. }
  56. attempt.failures++
  57. l.pruneLocked(now)
  58. if attempt.failures >= l.maxFailures {
  59. attempt.lockedUntil = now.Add(l.lockout)
  60. attempt.failures = 0
  61. attempt.firstFail = now
  62. return l.lockout, true
  63. }
  64. return 0, false
  65. }
  66. func (l *loginRateLimiter) recordSuccess(key string) {
  67. l.mu.Lock()
  68. defer l.mu.Unlock()
  69. delete(l.attempts, key)
  70. }
  71. // pruneLocked drops entries that are neither locked nor accumulating, so the
  72. // map stays bounded. Callers must hold the lock.
  73. func (l *loginRateLimiter) pruneLocked(now time.Time) {
  74. if len(l.attempts) < 1024 {
  75. return
  76. }
  77. for key, attempt := range l.attempts {
  78. if now.After(attempt.lockedUntil) && now.Sub(attempt.firstFail) > l.window {
  79. delete(l.attempts, key)
  80. }
  81. }
  82. }