detached 086e0df

feat: Enhance Starskey rate limiter with IP jail reporting and management

- Add periodic reporting of jailed IPs every 5 minutes
- Implement startup reporting of jailed IPs count
- Improve jail status tracking with more detailed logging
- Add manual reset logging for jailed IPs
- Introduce stopReports channel for graceful goroutine termination
- Enhance Allow method with more comprehensive jail status checks
commit
086e0dfe4e6c37897b9c2c51118f968a65308b30
author
bamen · · 2025-03-10 14:03 UTC
parent
6b4468e
signature
unsigned
1 file · +83 -12
M pkg/kv/ratelimiter.go +83 -12
@@ -15,10 +15,11 @@ type RateLimitInfo struct {
1515 }
1616
1717 type StarskeyRateLimiterStore struct {
18- db *starskey.Starskey
19- rate float64
20- burst int
21- expiresIn time.Duration
18+ db *starskey.Starskey
19+ rate float64
20+ burst int
21+ expiresIn time.Duration
22+ stopReports chan struct{} // Channel to signal stopping the report goroutine
2223 }
2324
2425 // NewStarskeyRateLimiterStore creates a new rate limiter store backed by Starskey
@@ -47,12 +48,18 @@ func NewStarskeyRateLimiterStore(dbPath string, rate float64, burst int, expires
4748 "burst", burst,
4849 "expiration", expiresIn)
4950
50- return &StarskeyRateLimiterStore{
51- db: db,
52- rate: rate,
53- burst: burst,
54- expiresIn: expiresIn,
55- }, nil
51+ store := &StarskeyRateLimiterStore{
52+ db: db,
53+ rate: rate,
54+ burst: burst,
55+ expiresIn: expiresIn,
56+ stopReports: make(chan struct{}),
57+ }
58+
59+ // Start the jailed IPs reporting
60+ go store.startJailReporting()
61+
62+ return store, nil
5663 }
5764
5865 // Implement Echo's RateLimiterStore interface
@@ -84,6 +91,12 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
8491 return nil
8592 }
8693
94+ // If IP was jailed but now should be freed (reset time passed)
95+ if info.Jailed && !now.Before(info.ResetTime) {
96+ log.Info("IP address released from jail", "ip", identifier)
97+ info.Jailed = false
98+ }
99+
87100 // Calculate time-based allowance
88101 timePassed := now.Sub(info.ResetTime).Seconds()
89102 tokensToAdd := timePassed * s.rate
@@ -107,6 +120,8 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
107120 info.Jailed = true
108121 info.ResetTime = now.Add(time.Second) // 1 second penalty
109122 log.Debug("IP jailed (burst exceeded)", "ip", identifier, "reset_at", info.ResetTime)
123+ // Add info level log when an IP is jailed
124+ log.Info("IP address jailed due to rate limit violation", "ip", identifier, "reset_at", info.ResetTime)
110125 } else {
111126 log.Debug("Request allowed", "ip", identifier, "attempts", info.Attempts)
112127 }
@@ -134,7 +149,25 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
134149 }
135150
136151 func (s *StarskeyRateLimiterStore) Reset(identifier string) (bool, error) {
137- err := s.db.Delete([]byte(identifier))
152+ // Check if the IP was jailed before resetting
153+ var wasJailed bool
154+
155+ // Access data before deletion to check jail status
156+ value, err := s.db.Get([]byte(identifier))
157+ if err == nil && value != nil {
158+ var info RateLimitInfo
159+ if err := json.Unmarshal(value, &info); err == nil {
160+ wasJailed = info.Jailed
161+ }
162+ }
163+
164+ err = s.db.Delete([]byte(identifier))
165+
166+ // Log if we freed a jailed IP
167+ if wasJailed {
168+ log.Info("IP address manually reset and released from jail", "ip", identifier)
169+ }
170+
138171 return err == nil, err
139172 }
140173
@@ -177,9 +210,47 @@ func (s *StarskeyRateLimiterStore) GetJailedIPs() (map[string]time.Time, error)
177210 return jailed, nil
178211 }
179212
180-// Close closes the underlying Starskey database
213+// startJailReporting starts a goroutine that logs jailed IPs count on startup and every 5 minutes
214+func (s *StarskeyRateLimiterStore) startJailReporting() {
215+ // Initial report on startup
216+ s.reportJailedIPs()
217+
218+ // Set up ticker for periodic reporting (every 5 minutes)
219+ ticker := time.NewTicker(5 * time.Minute)
220+ defer ticker.Stop()
221+
222+ for {
223+ select {
224+ case <-ticker.C:
225+ s.reportJailedIPs()
226+ case <-s.stopReports:
227+ log.Debug("Stopping jailed IPs reporting")
228+ return
229+ }
230+ }
231+}
232+
233+// reportJailedIPs gets and logs the current jailed IPs count
234+func (s *StarskeyRateLimiterStore) reportJailedIPs() {
235+ jailed, err := s.GetJailedIPs()
236+ if err != nil {
237+ log.Error("Failed to get jailed IPs", "error", err)
238+ return
239+ }
240+
241+ count := len(jailed)
242+ if count > 0 {
243+ log.Info("Currently jailed IPs", "count", count)
244+ } else {
245+ log.Debug("No IPs currently in jail")
246+ }
247+}
248+
249+// Close closes the underlying Starskey database and stops the reporting goroutine
181250 func (s *StarskeyRateLimiterStore) Close() error {
182251 log.Debug("Closing rate limiter store")
252+ // Signal the reporting goroutine to stop
253+ close(s.stopReports)
183254 return s.db.Close()
184255 }
185256
@@ -15,10 +15,11 @@ type RateLimitInfo struct {
15 }15 }
16 16
17 type StarskeyRateLimiterStore struct {17 type StarskeyRateLimiterStore struct {
18- db *starskey.Starskey18+ db *starskey.Starskey
19- rate float6419+ rate float64
20- burst int20+ burst int
21- expiresIn time.Duration21+ expiresIn time.Duration
22+ stopReports chan struct{} // Channel to signal stopping the report goroutine
22 }23 }
23 24
24 // NewStarskeyRateLimiterStore creates a new rate limiter store backed by Starskey25 // NewStarskeyRateLimiterStore creates a new rate limiter store backed by Starskey
@@ -47,12 +48,18 @@ func NewStarskeyRateLimiterStore(dbPath string, rate float64, burst int, expires
47 "burst", burst,48 "burst", burst,
48 "expiration", expiresIn)49 "expiration", expiresIn)
49 50
50- return &StarskeyRateLimiterStore{51+ store := &StarskeyRateLimiterStore{
51- db: db,52+ db: db,
52- rate: rate,53+ rate: rate,
53- burst: burst,54+ burst: burst,
54- expiresIn: expiresIn,55+ expiresIn: expiresIn,
55- }, nil56+ stopReports: make(chan struct{}),
57+ }
58+
59+ // Start the jailed IPs reporting
60+ go store.startJailReporting()
61+
62+ return store, nil
56 }63 }
57 64
58 // Implement Echo's RateLimiterStore interface65 // Implement Echo's RateLimiterStore interface
@@ -84,6 +91,12 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
84 return nil91 return nil
85 }92 }
86 93
94+ // If IP was jailed but now should be freed (reset time passed)
95+ if info.Jailed && !now.Before(info.ResetTime) {
96+ log.Info("IP address released from jail", "ip", identifier)
97+ info.Jailed = false
98+ }
99+
87 // Calculate time-based allowance100 // Calculate time-based allowance
88 timePassed := now.Sub(info.ResetTime).Seconds()101 timePassed := now.Sub(info.ResetTime).Seconds()
89 tokensToAdd := timePassed * s.rate102 tokensToAdd := timePassed * s.rate
@@ -107,6 +120,8 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
107 info.Jailed = true120 info.Jailed = true
108 info.ResetTime = now.Add(time.Second) // 1 second penalty121 info.ResetTime = now.Add(time.Second) // 1 second penalty
109 log.Debug("IP jailed (burst exceeded)", "ip", identifier, "reset_at", info.ResetTime)122 log.Debug("IP jailed (burst exceeded)", "ip", identifier, "reset_at", info.ResetTime)
123+ // Add info level log when an IP is jailed
124+ log.Info("IP address jailed due to rate limit violation", "ip", identifier, "reset_at", info.ResetTime)
110 } else {125 } else {
111 log.Debug("Request allowed", "ip", identifier, "attempts", info.Attempts)126 log.Debug("Request allowed", "ip", identifier, "attempts", info.Attempts)
112 }127 }
@@ -134,7 +149,25 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
134 }149 }
135 150
136 func (s *StarskeyRateLimiterStore) Reset(identifier string) (bool, error) {151 func (s *StarskeyRateLimiterStore) Reset(identifier string) (bool, error) {
137- err := s.db.Delete([]byte(identifier))152+ // Check if the IP was jailed before resetting
153+ var wasJailed bool
154+
155+ // Access data before deletion to check jail status
156+ value, err := s.db.Get([]byte(identifier))
157+ if err == nil && value != nil {
158+ var info RateLimitInfo
159+ if err := json.Unmarshal(value, &info); err == nil {
160+ wasJailed = info.Jailed
161+ }
162+ }
163+
164+ err = s.db.Delete([]byte(identifier))
165+
166+ // Log if we freed a jailed IP
167+ if wasJailed {
168+ log.Info("IP address manually reset and released from jail", "ip", identifier)
169+ }
170+
138 return err == nil, err171 return err == nil, err
139 }172 }
140 173
@@ -177,9 +210,47 @@ func (s *StarskeyRateLimiterStore) GetJailedIPs() (map[string]time.Time, error)
177 return jailed, nil210 return jailed, nil
178 }211 }
179 212
180-// Close closes the underlying Starskey database213+// startJailReporting starts a goroutine that logs jailed IPs count on startup and every 5 minutes
214+func (s *StarskeyRateLimiterStore) startJailReporting() {
215+ // Initial report on startup
216+ s.reportJailedIPs()
217+
218+ // Set up ticker for periodic reporting (every 5 minutes)
219+ ticker := time.NewTicker(5 * time.Minute)
220+ defer ticker.Stop()
221+
222+ for {
223+ select {
224+ case <-ticker.C:
225+ s.reportJailedIPs()
226+ case <-s.stopReports:
227+ log.Debug("Stopping jailed IPs reporting")
228+ return
229+ }
230+ }
231+}
232+
233+// reportJailedIPs gets and logs the current jailed IPs count
234+func (s *StarskeyRateLimiterStore) reportJailedIPs() {
235+ jailed, err := s.GetJailedIPs()
236+ if err != nil {
237+ log.Error("Failed to get jailed IPs", "error", err)
238+ return
239+ }
240+
241+ count := len(jailed)
242+ if count > 0 {
243+ log.Info("Currently jailed IPs", "count", count)
244+ } else {
245+ log.Debug("No IPs currently in jail")
246+ }
247+}
248+
249+// Close closes the underlying Starskey database and stops the reporting goroutine
181 func (s *StarskeyRateLimiterStore) Close() error {250 func (s *StarskeyRateLimiterStore) Close() error {
182 log.Debug("Closing rate limiter store")251 log.Debug("Closing rate limiter store")
252+ // Signal the reporting goroutine to stop
253+ close(s.stopReports)
183 return s.db.Close()254 return s.db.Close()
184 }255 }
185 256

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close