Initial commit: Caddy SIP Guardian module
Layer 4 SIP protection with: - SIP traffic matching (REGISTER, INVITE, etc.) - Rate limiting and automatic IP banning - Attack pattern detection (sipvicious, friendly-scanner) - CIDR whitelisting - Admin API for ban management
This commit is contained in:
commit
1ba05e160c
12 changed files with 1888 additions and 0 deletions
167
admin.go
Normal file
167
admin.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package sipguardian
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(AdminHandler{})
|
||||
}
|
||||
|
||||
// AdminHandler provides HTTP endpoints to manage SIP Guardian
|
||||
type AdminHandler struct {
|
||||
guardian *SIPGuardian
|
||||
}
|
||||
|
||||
func (AdminHandler) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.handlers.sip_guardian_admin",
|
||||
New: func() caddy.Module { return new(AdminHandler) },
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) Provision(ctx caddy.Context) error {
|
||||
// Get the shared guardian instance
|
||||
// In production, this would use proper module loading
|
||||
h.guardian = &SIPGuardian{}
|
||||
return h.guardian.Provision(ctx)
|
||||
}
|
||||
|
||||
// ServeHTTP handles admin API requests
|
||||
func (h *AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
// Route based on path
|
||||
path := r.URL.Path
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(path, "/bans"):
|
||||
return h.handleBans(w, r)
|
||||
case strings.HasSuffix(path, "/stats"):
|
||||
return h.handleStats(w, r)
|
||||
case strings.Contains(path, "/unban/"):
|
||||
return h.handleUnban(w, r, path)
|
||||
case strings.Contains(path, "/ban/"):
|
||||
return h.handleBan(w, r, path)
|
||||
default:
|
||||
return next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// handleBans lists all banned IPs
|
||||
func (h *AdminHandler) handleBans(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
|
||||
bans := h.guardian.GetBannedIPs()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"bans": bans,
|
||||
"count": len(bans),
|
||||
})
|
||||
}
|
||||
|
||||
// handleStats returns current statistics
|
||||
func (h *AdminHandler) handleStats(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
|
||||
stats := h.guardian.GetStats()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return json.NewEncoder(w).Encode(stats)
|
||||
}
|
||||
|
||||
// handleUnban removes an IP from the ban list
|
||||
func (h *AdminHandler) handleUnban(w http.ResponseWriter, r *http.Request, path string) error {
|
||||
if r.Method != http.MethodPost && r.Method != http.MethodDelete {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract IP from path: /admin/sip-guardian/unban/{ip}
|
||||
parts := strings.Split(path, "/unban/")
|
||||
if len(parts) != 2 || parts[1] == "" {
|
||||
http.Error(w, "IP address required", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
ip := strings.TrimSuffix(parts[1], "/")
|
||||
|
||||
if h.guardian.UnbanIP(ip) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "IP unbanned",
|
||||
"ip": ip,
|
||||
})
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "IP not found in ban list",
|
||||
"ip": ip,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleBan manually adds an IP to the ban list
|
||||
func (h *AdminHandler) handleBan(w http.ResponseWriter, r *http.Request, path string) error {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract IP from path: /admin/sip-guardian/ban/{ip}
|
||||
parts := strings.Split(path, "/ban/")
|
||||
if len(parts) != 2 || parts[1] == "" {
|
||||
http.Error(w, "IP address required", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
ip := strings.TrimSuffix(parts[1], "/")
|
||||
|
||||
// Parse optional reason from body
|
||||
var body struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if r.Body != nil {
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
}
|
||||
if body.Reason == "" {
|
||||
body.Reason = "manual_ban"
|
||||
}
|
||||
|
||||
// Force a ban by recording max failures
|
||||
h.guardian.mu.Lock()
|
||||
h.guardian.failureCounts[ip] = &failureTracker{
|
||||
count: h.guardian.MaxFailures,
|
||||
}
|
||||
h.guardian.banIP(ip, body.Reason)
|
||||
h.guardian.mu.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "IP banned",
|
||||
"ip": ip,
|
||||
"reason": body.Reason,
|
||||
})
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddyhttp.MiddlewareHandler = (*AdminHandler)(nil)
|
||||
_ caddy.Provisioner = (*AdminHandler)(nil)
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue