Frontend Labs
⋯

Day 98 — UDP Amplification Defense

Learning objective Build a UDP service that cannot be used as an amplification vector
Key packages net, sync, time, sync/atomic
Security context Amplification factor = response_bytes / request_bytes; services with factor >1 can be weaponized

Key Concepts

Amplification attack: Attacker sends small UDP packet with spoofed source IP (victim). Service replies with large response to victim. Factor 10x = 10Gbps attack from 1Gbps attacker.

Defenses:

  1. Response size cap: Never send more bytes than received × 1.5
  2. Per-source rate limiting: Max 10 UDP responses per second per source IP
  3. Challenge-response: Require a handshake before sending large responses

Exercise — Anti-Amplification UDP Server

package ddos

import (
    "crypto/sha256"
    "encoding/binary"
    "fmt"
    "net"
    "sync"
    "sync/atomic"
    "time"
)

type udpRateEntry struct {
    mu      sync.Mutex
    tokens  float64
    lastSee time.Time
    blocked atomic.Int32
}

// AntiAmpUDPServer is a UDP server that enforces anti-amplification rules
type AntiAmpUDPServer struct {
    conn        *net.UDPConn
    maxFactor   float64 // max response_size / request_size ratio (e.g. 1.5)
    ratePerIP   float64 // max responses per second per source IP
    mu          sync.Mutex
    ipBuckets   map[string]*udpRateEntry
    blocked     map[string]time.Time
    ampAttempts atomic.Int64
}

func NewAntiAmpUDPServer(addr string, maxFactor, ratePerIP float64) (*AntiAmpUDPServer, error) {
    udpAddr, err := net.ResolveUDPAddr("udp", addr)
    if err != nil {
        return nil, err
    }
    conn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        return nil, err
    }
    s := &AntiAmpUDPServer{
        conn:      conn,
        maxFactor: maxFactor,
        ratePerIP: ratePerIP,
        ipBuckets: make(map[string]*udpRateEntry),
        blocked:   make(map[string]time.Time),
    }
    return s, nil
}

func (s *AntiAmpUDPServer) Serve() {
    buf := make([]byte, 1500) // MTU-sized buffer
    for {
        n, src, err := s.conn.ReadFromUDP(buf)
        if err != nil {
            return
        }
        reqBytes := n
        ip := src.IP.String()

        if s.isBlocked(ip) {
            continue // silently drop — no amplification
        }
        if !s.allowRate(ip) {
            s.checkBlock(ip)
            continue
        }

        // Process request and generate response
        response := s.process(buf[:n])

        // Anti-amplification: cap response to maxFactor × request size
        maxRespSize := int(float64(reqBytes) * s.maxFactor)
        if len(response) > maxRespSize {
            s.ampAttempts.Add(1)
            response = response[:maxRespSize] // truncate
        }

        s.conn.WriteToUDP(response, src)
    }
}

func (s *AntiAmpUDPServer) process(req []byte) []byte {
    // Example: echo service with SHA256 response header (simulated work)
    h := sha256.Sum256(req)
    resp := make([]byte, 4+len(req))
    binary.BigEndian.PutUint32(resp[:4], uint32(len(req)))
    copy(resp[4:], h[:])
    return resp
}

func (s *AntiAmpUDPServer) allowRate(ip string) bool {
    s.mu.Lock()
    entry, ok := s.ipBuckets[ip]
    if !ok {
        entry = &udpRateEntry{tokens: s.ratePerIP, lastSee: time.Now()}
        s.ipBuckets[ip] = entry
    }
    s.mu.Unlock()

    entry.mu.Lock()
    defer entry.mu.Unlock()
    now := time.Now()
    elapsed := now.Sub(entry.lastSee).Seconds()
    entry.lastSee = now
    entry.tokens = minF(s.ratePerIP, entry.tokens+elapsed*s.ratePerIP)
    if entry.tokens < 1 {
        return false
    }
    entry.tokens--
    return true
}

func (s *AntiAmpUDPServer) checkBlock(ip string) {
    entry, ok := s.ipBuckets[ip]
    if !ok {
        return
    }
    entry.blocked.Add(1)
    if entry.blocked.Load() >= 2 {
        s.mu.Lock()
        s.blocked[ip] = time.Now().Add(5 * time.Minute)
        s.mu.Unlock()
    }
}

func (s *AntiAmpUDPServer) isBlocked(ip string) bool {
    s.mu.Lock()
    exp, ok := s.blocked[ip]
    s.mu.Unlock()
    return ok && time.Now().Before(exp)
}

func minF(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}

func (s *AntiAmpUDPServer) Stats() string {
    return fmt.Sprintf("amp_attempts=%d", s.ampAttempts.Load())
}

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