Frontend Labs
⋯

Day 49 — Rate Limiting at L4

Learning Objectives

  • Limit connections per source IP — first line of DDoS defense
  • Use sync/atomic for 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.


Generated from week7.md · 8 Oct 2026 16:36