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:
- Response size cap: Never send more bytes than received × 1.5
- Per-source rate limiting: Max 10 UDP responses per second per source IP
- 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())
}