invalid_auth_abuse_limiter.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package service
  2. import (
  3. "sync"
  4. "sync/atomic"
  5. "time"
  6. "github.com/Wei-Shaw/sub2api/internal/config"
  7. )
  8. const invalidAuthAbuseShardCount = 16
  9. type invalidAuthAbuseEntry struct {
  10. failures int
  11. windowStart time.Time
  12. blockedUntil time.Time
  13. }
  14. type invalidAuthAbuseShard struct {
  15. mu sync.Mutex
  16. entries map[string]*invalidAuthAbuseEntry
  17. }
  18. type invalidAuthOverflow struct {
  19. mu sync.Mutex
  20. failures int
  21. windowStart time.Time
  22. blockedUntil time.Time
  23. }
  24. type invalidAuthAbuseLimiter struct {
  25. threshold int
  26. window time.Duration
  27. block time.Duration
  28. capacity int64
  29. shards [invalidAuthAbuseShardCount]invalidAuthAbuseShard
  30. overflow invalidAuthOverflow
  31. now func() time.Time
  32. tracked atomic.Int64
  33. recorded atomic.Uint64
  34. blocked atomic.Uint64
  35. rejected atomic.Uint64
  36. expired atomic.Uint64
  37. overflowed atomic.Uint64
  38. globalBlocked atomic.Uint64
  39. cleanupNext atomic.Int64
  40. cleanupCursor atomic.Uint32
  41. }
  42. type InvalidAuthAbuseHealth struct {
  43. Enabled bool `json:"enabled"`
  44. Tracked int64 `json:"tracked"`
  45. Capacity int64 `json:"capacity"`
  46. Recorded uint64 `json:"recorded"`
  47. Blocks uint64 `json:"blocks"`
  48. Rejected uint64 `json:"rejected"`
  49. Expired uint64 `json:"expired"`
  50. Overflowed uint64 `json:"overflowed"`
  51. GlobalBlocked uint64 `json:"global_blocked"`
  52. }
  53. func newInvalidAuthAbuseLimiter(cfg *config.Config) *invalidAuthAbuseLimiter {
  54. if cfg == nil || !cfg.APIKeyAuth.InvalidAbuse.Enabled {
  55. return nil
  56. }
  57. c := cfg.APIKeyAuth.InvalidAbuse
  58. if c.Threshold <= 0 || c.WindowSeconds <= 0 || c.BlockSeconds <= 0 || c.Capacity <= 0 {
  59. return nil
  60. }
  61. l := &invalidAuthAbuseLimiter{
  62. threshold: c.Threshold,
  63. window: time.Duration(c.WindowSeconds) * time.Second,
  64. block: time.Duration(c.BlockSeconds) * time.Second,
  65. capacity: int64(c.Capacity),
  66. now: time.Now,
  67. }
  68. for i := range l.shards {
  69. l.shards[i].entries = make(map[string]*invalidAuthAbuseEntry)
  70. }
  71. return l
  72. }
  73. func (s *APIKeyService) CheckInvalidAuthAbuse(clientKey string) (time.Duration, bool) {
  74. if s == nil || s.invalidAuthAbuse == nil {
  75. return 0, false
  76. }
  77. return s.invalidAuthAbuse.check(clientKey)
  78. }
  79. func (s *APIKeyService) RecordInvalidAuthFailure(clientKey string) {
  80. if s == nil || s.invalidAuthAbuse == nil {
  81. return
  82. }
  83. s.invalidAuthAbuse.record(clientKey)
  84. }
  85. func (s *APIKeyService) InvalidAuthAbuseHealth() InvalidAuthAbuseHealth {
  86. if s == nil || s.invalidAuthAbuse == nil {
  87. return InvalidAuthAbuseHealth{}
  88. }
  89. return s.invalidAuthAbuse.health()
  90. }
  91. func (l *invalidAuthAbuseLimiter) check(clientKey string) (time.Duration, bool) {
  92. if l == nil || clientKey == "" {
  93. return 0, false
  94. }
  95. now := l.now()
  96. l.maybeCleanupAtCapacity(now)
  97. shard := l.shard(clientKey)
  98. shard.mu.Lock()
  99. entry := shard.entries[clientKey]
  100. if entry != nil && l.entryExpired(entry, now) {
  101. delete(shard.entries, clientKey)
  102. l.tracked.Add(-1)
  103. l.expired.Add(1)
  104. entry = nil
  105. }
  106. if entry != nil && entry.blockedUntil.After(now) {
  107. retry := entry.blockedUntil.Sub(now)
  108. shard.mu.Unlock()
  109. l.rejected.Add(1)
  110. return retry, true
  111. }
  112. shard.mu.Unlock()
  113. if entry == nil && l.tracked.Load() >= l.capacity {
  114. if retry, blocked := l.checkOverflow(now); blocked {
  115. l.rejected.Add(1)
  116. l.globalBlocked.Add(1)
  117. return retry, true
  118. }
  119. }
  120. return 0, false
  121. }
  122. func (l *invalidAuthAbuseLimiter) record(clientKey string) {
  123. if l == nil || clientKey == "" {
  124. return
  125. }
  126. l.recorded.Add(1)
  127. now := l.now()
  128. l.maybeCleanupAtCapacity(now)
  129. shard := l.shard(clientKey)
  130. shard.mu.Lock()
  131. entry := shard.entries[clientKey]
  132. if entry != nil && l.entryExpired(entry, now) {
  133. delete(shard.entries, clientKey)
  134. l.tracked.Add(-1)
  135. l.expired.Add(1)
  136. entry = nil
  137. }
  138. if entry == nil {
  139. if !l.reserveEntry() {
  140. shard.mu.Unlock()
  141. l.recordOverflow(now)
  142. return
  143. }
  144. entry = &invalidAuthAbuseEntry{windowStart: now}
  145. shard.entries[clientKey] = entry
  146. }
  147. if entry.blockedUntil.After(now) {
  148. shard.mu.Unlock()
  149. return
  150. }
  151. if entry.windowStart.After(now) || !now.Before(entry.windowStart.Add(l.window)) {
  152. entry.windowStart = now
  153. entry.failures = 0
  154. }
  155. entry.failures++
  156. if entry.failures >= l.threshold {
  157. entry.failures = 0
  158. entry.blockedUntil = now.Add(l.block)
  159. entry.windowStart = entry.blockedUntil
  160. l.blocked.Add(1)
  161. }
  162. shard.mu.Unlock()
  163. }
  164. func (l *invalidAuthAbuseLimiter) reserveEntry() bool {
  165. for {
  166. current := l.tracked.Load()
  167. if current >= l.capacity {
  168. return false
  169. }
  170. if l.tracked.CompareAndSwap(current, current+1) {
  171. return true
  172. }
  173. }
  174. }
  175. func (l *invalidAuthAbuseLimiter) maybeCleanupAtCapacity(now time.Time) {
  176. if l.tracked.Load() < l.capacity {
  177. return
  178. }
  179. nowUnixNano := now.UnixNano()
  180. for {
  181. next := l.cleanupNext.Load()
  182. if nowUnixNano < next {
  183. return
  184. }
  185. if l.cleanupNext.CompareAndSwap(next, now.Add(100*time.Millisecond).UnixNano()) {
  186. break
  187. }
  188. }
  189. index := l.cleanupCursor.Add(1) - 1
  190. shard := &l.shards[index%invalidAuthAbuseShardCount]
  191. shard.mu.Lock()
  192. for key, entry := range shard.entries {
  193. if l.entryExpired(entry, now) {
  194. delete(shard.entries, key)
  195. l.tracked.Add(-1)
  196. l.expired.Add(1)
  197. }
  198. }
  199. shard.mu.Unlock()
  200. }
  201. func (l *invalidAuthAbuseLimiter) entryExpired(entry *invalidAuthAbuseEntry, now time.Time) bool {
  202. return entry != nil && !entry.blockedUntil.After(now) && !entry.windowStart.After(now) && !now.Before(entry.windowStart.Add(l.window))
  203. }
  204. func (l *invalidAuthAbuseLimiter) shard(clientKey string) *invalidAuthAbuseShard {
  205. const fnvOffset32 = uint32(2166136261)
  206. const fnvPrime32 = uint32(16777619)
  207. hash := fnvOffset32
  208. for i := 0; i < len(clientKey); i++ {
  209. hash ^= uint32(clientKey[i])
  210. hash *= fnvPrime32
  211. }
  212. return &l.shards[hash%invalidAuthAbuseShardCount]
  213. }
  214. func (l *invalidAuthAbuseLimiter) recordOverflow(now time.Time) {
  215. l.overflowed.Add(1)
  216. l.overflow.mu.Lock()
  217. defer l.overflow.mu.Unlock()
  218. if l.overflow.blockedUntil.After(now) {
  219. return
  220. }
  221. if l.overflow.windowStart.IsZero() || !now.Before(l.overflow.windowStart.Add(l.window)) {
  222. l.overflow.windowStart = now
  223. l.overflow.failures = 0
  224. }
  225. l.overflow.failures++
  226. if l.overflow.failures >= l.threshold {
  227. l.overflow.failures = 0
  228. l.overflow.blockedUntil = now.Add(l.block)
  229. l.overflow.windowStart = l.overflow.blockedUntil
  230. l.blocked.Add(1)
  231. }
  232. }
  233. func (l *invalidAuthAbuseLimiter) checkOverflow(now time.Time) (time.Duration, bool) {
  234. l.overflow.mu.Lock()
  235. defer l.overflow.mu.Unlock()
  236. if l.overflow.blockedUntil.After(now) {
  237. return l.overflow.blockedUntil.Sub(now), true
  238. }
  239. return 0, false
  240. }
  241. func (l *invalidAuthAbuseLimiter) health() InvalidAuthAbuseHealth {
  242. return InvalidAuthAbuseHealth{
  243. Enabled: true,
  244. Tracked: l.tracked.Load(),
  245. Capacity: l.capacity,
  246. Recorded: l.recorded.Load(),
  247. Blocks: l.blocked.Load(),
  248. Rejected: l.rejected.Load(),
  249. Expired: l.expired.Load(),
  250. Overflowed: l.overflowed.Load(),
  251. GlobalBlocked: l.globalBlocked.Load(),
  252. }
  253. }