Day 49 — Rate Limiting at L4
Learning Objectives
- Limit connections per source IP — first line of DDoS defense
- Use
sync/atomicfor lock-free counters in the accept loop - Expose rejection metrics for anomaly detection
Exercise: Per-IP Connection Rate Limiter
package main
import (
"fmt"
"net"
"sync"
"sync/atomic"
"time"
)
type IPLimiter struct {
mu sync.Mutex
perIP map[string]*ipState
globalActive atomic.Int64
globalMax int64
perIPMax int64
cleanupTick *time.Ticker
}
type ipState struct {
active atomic.Int64
total atomic.Int64
lastSeen time.Time
}
func NewIPLimiter(globalMax, perIPMax int64) *IPLimiter {
l := &IPLimiter{
perIP: make(map[string]*ipState),
globalMax: globalMax,
perIPMax: perIPMax,
cleanupTick: time.NewTicker(5 * time.Minute),
}
go l.cleanup()
return l
}
func (l *IPLimiter) Allow(conn net.Conn) bool {
// Global limit check (lock-free)
if l.globalActive.Load() >= l.globalMax {
return false
}
ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
l.mu.Lock()
state, ok := l.perIP[ip]
if !ok {
state = &ipState{}
l.perIP[ip] = state
}
state.lastSeen = time.Now()
l.mu.Unlock()
// Per-IP limit check
if state.active.Load() >= l.perIPMax {
return false
}
l.globalActive.Add(1)
state.active.Add(1)
state.total.Add(1)
return true
}
func (l *IPLimiter) Release(conn net.Conn) {
ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
l.globalActive.Add(-1)
l.mu.Lock()
if state, ok := l.perIP[ip]; ok {
state.active.Add(-1)
}
l.mu.Unlock()
}
func (l *IPLimiter) cleanup() {
for range l.cleanupTick.C {
cutoff := time.Now().Add(-10 * time.Minute)
l.mu.Lock()
for ip, state := range l.perIP {
if state.lastSeen.Before(cutoff) && state.active.Load() == 0 {
delete(l.perIP, ip)
}
}
l.mu.Unlock()
}
}
func (l *IPLimiter) Stats() map[string]int64 {
return map[string]int64{
"global_active": l.globalActive.Load(),
"global_max": l.globalMax,
}
}
// Integration with the TCP server
type RateLimitedServer struct {
listener net.Listener
limiter *IPLimiter
}
func (s *RateLimitedServer) Serve() {
for {
conn, err := s.listener.Accept()
if err != nil { return }
if !s.limiter.Allow(conn) {
// Immediately close — don't waste server resources
conn.Close()
fmt.Printf("[RATELIMIT] rejected %s\n", conn.RemoteAddr())
continue
}
go func() {
defer s.limiter.Release(conn)
defer conn.Close()
// handle connection...
time.Sleep(100 * time.Millisecond)
}()
}
}
func main() {
limiter := NewIPLimiter(1000, 100) // 1000 global, 100 per IP
ln, _ := net.Listen("tcp", ":8080")
srv := &RateLimitedServer{listener: ln, limiter: limiter}
fmt.Println("Rate-limited server on :8080 (1000 global, 100/IP)")
srv.Serve()
}
Real-World Context
This is the first mitigation layer against DDoS. When Cloudflare detects a SYN flood, they drop packets at L3/L4 before they even reach the application. Your proxy can't do L3 packet dropping, but it can reject L4 connections immediately — "fail fast" to protect backend resources.