49. A WAF-style middleware: rate limiting and IP blocklists
Goal: extend Chapter 39's gateway into something closer to a real Web Application Firewall layer — inspecting and gating requests before they reach the application, the same territory as this Wiki's WAF/DDoS security material, implemented as ordinary Go middleware.
package waf
import (
"net"
"net/http"
"strings"
"sync"
"golang.org/x/time/rate"
)
type Firewall struct {
mu sync.RWMutex
blocklist map[string]bool
limiters map[string]*rate.Limiter
rps rate.Limit
burst int
}
func New(rps float64, burst int) *Firewall {
return &Firewall{
blocklist: map[string]bool{},
limiters: map[string]*rate.Limiter{},
rps: rate.Limit(rps),
burst: burst,
}
}
func (f *Firewall) Block(ip string) {
f.mu.Lock()
defer f.mu.Unlock()
f.blocklist[ip] = true
}
func (f *Firewall) isBlocked(ip string) bool {
f.mu.RLock()
defer f.mu.RUnlock()
return f.blocklist[ip]
}
func (f *Firewall) limiterFor(ip string) *rate.Limiter {
f.mu.Lock()
defer f.mu.Unlock()
lim, ok := f.limiters[ip]
if !ok {
lim = rate.NewLimiter(f.rps, f.burst)
f.limiters[ip] = lim
}
return lim
}
// suspiciousPatterns is a minimal illustration — a real WAF uses a
// maintained ruleset (e.g. OWASP CRS), not a hardcoded list.
var suspiciousPatterns = []string{"../", "<script", "UNION SELECT", "; DROP TABLE"}
func looksLikeAttack(r *http.Request) bool {
target := r.URL.RawQuery + " " + r.URL.Path
for _, p := range suspiciousPatterns {
if strings.Contains(strings.ToLower(target), strings.ToLower(p)) {
return true
}
}
return false
}
func (f *Firewall) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if f.isBlocked(ip) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if looksLikeAttack(r) {
f.Block(ip) // escalate: one bad request blocks this IP going forward
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if !f.limiterFor(ip).Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
fw := waf.New(10, 20) // 10 req/s, burst 20, per source IP
mux := http.NewServeMux()
mux.HandleFunc("/", appHandler)
log.Fatal(http.ListenAndServe(":8080", fw.Middleware(mux)))
}
This is illustrative, not production-grade — a real deployment puts this
kind of logic behind (or alongside) a dedicated reverse proxy/CDN layer,
uses a maintained signature set instead of a hardcoded pattern list, and
persists the blocklist outside process memory so a restart doesn't wipe
it. The point worth internalizing is architectural: the firewall is
just another http.Handler-wrapping-http.Handler middleware, composed
in front of your application exactly like logging or auth.