Frontend Labs
⋯

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.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36