Skip to content

Commit cd6c6ac

Browse files
committed
fix: remove scan guard causing self-ban
1 parent afb7c39 commit cd6c6ac

3 files changed

Lines changed: 2 additions & 168 deletions

File tree

main.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,8 @@ func runServe() {
227227
mux.Handle("GET /ops/events/stats", requireAgentKey(http.HandlerFunc(handleOpsEventStats)))
228228
mux.Handle("GET /ops/nodes", requireAgentKey(http.HandlerFunc(handleOpsNodeList)))
229229

230-
// Catch-all: any unmatched path is a scan probe
231-
mux.HandleFunc("/", scanNotFound)
232-
233-
// Global middleware: scan guard → access log → security headers → body limit → routes
234-
handler := scanGuard(accessLog(securityHeaders(maxBody(mux))))
230+
// Global middleware: access log → security headers → body limit → routes
231+
handler := accessLog(securityHeaders(maxBody(mux)))
235232

236233
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
237234
defer stop()

middleware.go

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -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

236146
type statusRecorder struct {

middleware_test.go

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -307,76 +307,3 @@ func TestRateLimit(t *testing.T) {
307307
}
308308
}
309309

310-
func resetScanner() {
311-
scanner.mu.Lock()
312-
scanner.hits = make(map[string]*window)
313-
scanner.banned = make(map[string]time.Time)
314-
scanner.mu.Unlock()
315-
}
316-
317-
func TestScanGuard_BansAfterThreshold(t *testing.T) {
318-
initDB(":memory:")
319-
cfg = &Config{
320-
AccessSecret: "test-access-secret-that-is-32-chars!!",
321-
RefreshSecret: "test-refresh-secret-that-is-32-chars!",
322-
}
323-
resetScanner()
324-
325-
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
326-
http.NotFound(w, r)
327-
})
328-
handler := scanGuard(inner)
329-
ts := httptest.NewServer(handler)
330-
defer ts.Close()
331-
332-
// First scanThreshold requests should get 404
333-
for i := 0; i < scanThreshold; i++ {
334-
resp, err := http.Get(ts.URL + "/probe.php")
335-
if err != nil {
336-
t.Fatalf("request %d: %v", i, err)
337-
}
338-
resp.Body.Close()
339-
if resp.StatusCode != http.StatusNotFound {
340-
t.Errorf("request %d: status = %d, want %d", i, resp.StatusCode, http.StatusNotFound)
341-
}
342-
}
343-
344-
// Next request should be banned (403 or connection closed)
345-
resp, err := http.Get(ts.URL + "/another.php")
346-
if err != nil {
347-
// Connection closed is acceptable (hijack)
348-
return
349-
}
350-
resp.Body.Close()
351-
if resp.StatusCode == http.StatusNotFound {
352-
t.Errorf("post-ban request got 404, expected ban (403 or connection close)")
353-
}
354-
}
355-
356-
func TestScanGuard_AllowsNormalTraffic(t *testing.T) {
357-
initDB(":memory:")
358-
cfg = &Config{
359-
AccessSecret: "test-access-secret-that-is-32-chars!!",
360-
RefreshSecret: "test-refresh-secret-that-is-32-chars!",
361-
}
362-
resetScanner()
363-
364-
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
365-
w.WriteHeader(http.StatusOK)
366-
})
367-
handler := scanGuard(inner)
368-
ts := httptest.NewServer(handler)
369-
defer ts.Close()
370-
371-
// 200 responses should never trigger a ban
372-
for i := 0; i < 20; i++ {
373-
resp, err := http.Get(ts.URL + "/")
374-
if err != nil {
375-
t.Fatalf("request %d: %v", i, err)
376-
}
377-
resp.Body.Close()
378-
if resp.StatusCode != http.StatusOK {
379-
t.Errorf("request %d: status = %d, want %d", i, resp.StatusCode, http.StatusOK)
380-
}
381-
}
382-
}

0 commit comments

Comments
 (0)