@@ -141,96 +141,6 @@ func requireAgentKey(next http.Handler) http.Handler {
141141 })
142142}
143143
144- // --- Scan detection (ban IPs that generate excessive 404s) ---
145-
146- const (
147- scanWindow = 1 * time .Minute
148- scanThreshold = 5
149- scanBanDur = 5 * time .Minute
150- maxScanKeys = 10_000
151- )
152-
153- var scanner = struct {
154- mu sync.Mutex
155- hits map [string ]* window
156- banned map [string ]time.Time
157- }{
158- hits : make (map [string ]* window ),
159- banned : make (map [string ]time.Time ),
160- }
161-
162- func init () {
163- go func () {
164- for range time .Tick (1 * time .Minute ) {
165- scanner .mu .Lock ()
166- now := timeNow ()
167- for k , w := range scanner .hits {
168- if now .After (w .resetAt ) {
169- delete (scanner .hits , k )
170- }
171- }
172- for k , exp := range scanner .banned {
173- if now .After (exp ) {
174- delete (scanner .banned , k )
175- }
176- }
177- scanner .mu .Unlock ()
178- }
179- }()
180- }
181-
182- func scanGuard (next http.Handler ) http.Handler {
183- return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
184- ip := clientIP (r )
185-
186- scanner .mu .Lock ()
187- if exp , ok := scanner .banned [ip ]; ok && timeNow ().Before (exp ) {
188- scanner .mu .Unlock ()
189- slog .Warn ("banned IP dropped" , "ip" , ip , "path" , r .URL .Path , "remaining" , time .Until (exp ).Round (time .Second ))
190- // Silent drop — no response body, no headers
191- hj , ok := w .(http.Hijacker )
192- if ok {
193- conn , _ , err := hj .Hijack ()
194- if err == nil {
195- conn .Close ()
196- return
197- }
198- }
199- // Fallback if hijack unavailable
200- w .WriteHeader (http .StatusForbidden )
201- return
202- }
203- scanner .mu .Unlock ()
204-
205- next .ServeHTTP (w , r )
206- })
207- }
208-
209- func scanNotFound (w http.ResponseWriter , r * http.Request ) {
210- ip := clientIP (r )
211- scanner .mu .Lock ()
212- now := timeNow ()
213- hit , ok := scanner .hits [ip ]
214- if ! ok || now .After (hit .resetAt ) {
215- if ! ok && len (scanner .hits ) >= maxScanKeys {
216- scanner .mu .Unlock ()
217- http .NotFound (w , r )
218- return
219- }
220- hit = & window {count : 0 , resetAt : now .Add (scanWindow )}
221- scanner .hits [ip ] = hit
222- }
223- hit .count ++
224- if hit .count >= scanThreshold {
225- scanner .banned [ip ] = now .Add (scanBanDur )
226- delete (scanner .hits , ip )
227- slog .Warn ("scan detected, IP banned" , "ip" , ip , "path" , r .URL .Path , "ua" , r .UserAgent (), "duration" , scanBanDur )
228- emitEvent ("scan.banned" , ip , 0 , r .UserAgent (), 403 , map [string ]any {"path" : r .URL .Path })
229- }
230- scanner .mu .Unlock ()
231- http .NotFound (w , r )
232- }
233-
234144// --- Access logging ---
235145
236146type statusRecorder struct {
0 commit comments