1
0

login_limiter.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. package controller
  2. import (
  3. "strings"
  4. "sync"
  5. "time"
  6. )
  7. const (
  8. loginLimitMaxFailures = 5
  9. loginLimitWindow = 5 * time.Minute
  10. loginLimitCooldown = 15 * time.Minute
  11. // Hard ceiling on tracked (ip, username) records. The key includes the
  12. // caller-supplied username, so an unauthenticated attacker rotating
  13. // usernames would otherwise grow the map without bound.
  14. loginLimitMaxRecords = 10000
  15. )
  16. var defaultLoginLimiter = newLoginLimiter(loginLimitMaxFailures, loginLimitWindow, loginLimitCooldown)
  17. type loginLimiter struct {
  18. mu sync.Mutex
  19. now func() time.Time
  20. maxFailures int
  21. window time.Duration
  22. cooldown time.Duration
  23. attempts map[string]*loginLimitRecord
  24. }
  25. type loginLimitRecord struct {
  26. failures []time.Time
  27. blockedUntil time.Time
  28. }
  29. func newLoginLimiter(maxFailures int, window, cooldown time.Duration) *loginLimiter {
  30. return &loginLimiter{
  31. now: time.Now,
  32. maxFailures: maxFailures,
  33. window: window,
  34. cooldown: cooldown,
  35. attempts: make(map[string]*loginLimitRecord),
  36. }
  37. }
  38. func (l *loginLimiter) allow(ip, username string) (time.Time, bool) {
  39. l.mu.Lock()
  40. defer l.mu.Unlock()
  41. key := loginLimitKey(ip, username)
  42. record := l.attempts[key]
  43. if record == nil {
  44. return time.Time{}, true
  45. }
  46. now := l.now()
  47. if now.Before(record.blockedUntil) {
  48. return record.blockedUntil, false
  49. }
  50. record.blockedUntil = time.Time{}
  51. record.failures = pruneLoginFailures(record.failures, now.Add(-l.window))
  52. if len(record.failures) == 0 {
  53. delete(l.attempts, key)
  54. }
  55. return time.Time{}, true
  56. }
  57. func (l *loginLimiter) registerFailure(ip, username string) (time.Time, bool) {
  58. l.mu.Lock()
  59. defer l.mu.Unlock()
  60. now := l.now()
  61. key := loginLimitKey(ip, username)
  62. record := l.attempts[key]
  63. if record == nil {
  64. l.evictForRoom(now)
  65. record = &loginLimitRecord{}
  66. l.attempts[key] = record
  67. }
  68. record.failures = pruneLoginFailures(record.failures, now.Add(-l.window))
  69. record.failures = append(record.failures, now)
  70. if len(record.failures) >= l.maxFailures {
  71. record.failures = nil
  72. record.blockedUntil = now.Add(l.cooldown)
  73. return record.blockedUntil, true
  74. }
  75. return time.Time{}, false
  76. }
  77. func (l *loginLimiter) registerSuccess(ip, username string) {
  78. l.mu.Lock()
  79. defer l.mu.Unlock()
  80. delete(l.attempts, loginLimitKey(ip, username))
  81. }
  82. // evictForRoom keeps the attempts map bounded before inserting a new record.
  83. // It first reclaims records that are no longer blocked and whose failures have
  84. // aged out of the window; if the map is still at the ceiling (a genuine
  85. // broad flood), it drops one arbitrary record so memory can never grow past the
  86. // cap. Callers hold l.mu.
  87. func (l *loginLimiter) evictForRoom(now time.Time) {
  88. if len(l.attempts) < loginLimitMaxRecords {
  89. return
  90. }
  91. cutoff := now.Add(-l.window)
  92. for key, record := range l.attempts {
  93. if now.Before(record.blockedUntil) {
  94. continue
  95. }
  96. record.failures = pruneLoginFailures(record.failures, cutoff)
  97. if len(record.failures) == 0 {
  98. delete(l.attempts, key)
  99. }
  100. }
  101. if len(l.attempts) < loginLimitMaxRecords {
  102. return
  103. }
  104. for key, record := range l.attempts {
  105. if now.Before(record.blockedUntil) {
  106. continue
  107. }
  108. delete(l.attempts, key)
  109. return
  110. }
  111. for key := range l.attempts {
  112. delete(l.attempts, key)
  113. return
  114. }
  115. }
  116. func loginLimitKey(ip, username string) string {
  117. return strings.TrimSpace(ip) + "\x00" + strings.ToLower(strings.TrimSpace(username))
  118. }
  119. func pruneLoginFailures(failures []time.Time, cutoff time.Time) []time.Time {
  120. keepFrom := 0
  121. for keepFrom < len(failures) && failures[keepFrom].Before(cutoff) {
  122. keepFrom++
  123. }
  124. return failures[keepFrom:]
  125. }