Day 34 — UDP
Learning Objectives
- Build a statsd-compatible metrics receiver — understand UDP's fire-and-forget nature
- Understand why UDP is used for DDoS amplification attacks
- Handle packet loss gracefully
Key Concepts
TCP vs UDP:
TCP: connection-oriented, ordered, reliable, flow-controlled
UDP: connectionless, unordered, unreliable, no flow control
Why UDP for metrics (statsd, syslog):
- Sending metrics should never block the application
- Lost metrics are acceptable; blocking is not
- 10k packets/sec is normal; TCP overhead would be significant
Why UDP enables DDoS amplification:
- Attacker spoofs source IP (UDP has no handshake to verify)
- Sends small request to DNS/NTP server
- Server sends large response to victim (the spoofed IP)
- Amplification factor: DNS up to 70x, NTP up to 556x
Exercise: statsd-Compatible Metrics Receiver
package main
import (
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
)
type Metric struct {
Name string
Value float64
Type string // c=counter, g=gauge, ms=timer
}
type StatsReceiver struct {
conn *net.UDPConn
mu sync.Mutex
counters map[string]float64
gauges map[string]float64
timers map[string][]float64
received int64
dropped int64
}
func NewStatsReceiver(addr string) (*StatsReceiver, 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
}
// Set receive buffer — important for burst handling
conn.SetReadBuffer(1024 * 1024) // 1MB
return &StatsReceiver{
conn: conn,
counters: make(map[string]float64),
gauges: make(map[string]float64),
timers: make(map[string][]float64),
}, nil
}
// parseMetric parses "metric.name:value|type" (statsd format)
func parseMetric(packet string) (*Metric, error) {
// Format: name:value|type[@sample_rate]
colonIdx := strings.LastIndex(packet, ":")
pipeIdx := strings.LastIndex(packet, "|")
if colonIdx < 0 || pipeIdx < colonIdx {
return nil, fmt.Errorf("invalid format: %q", packet)
}
name := packet[:colonIdx]
valueStr := packet[colonIdx+1 : pipeIdx]
metricType := strings.Split(packet[pipeIdx+1:], "@")[0]
value, err := strconv.ParseFloat(valueStr, 64)
if err != nil {
return nil, fmt.Errorf("invalid value %q", valueStr)
}
return &Metric{Name: name, Value: value, Type: metricType}, nil
}
func (sr *StatsReceiver) Receive() {
buf := make([]byte, 65507) // max UDP payload
for {
n, addr, err := sr.conn.ReadFromUDP(buf)
if err != nil {
return
}
_ = addr // could use for per-IP rate limiting
// Parse potentially multiple metrics (newline-separated)
for _, line := range strings.Split(string(buf[:n]), "\n") {
line = strings.TrimSpace(line)
if line == "" { continue }
m, err := parseMetric(line)
if err != nil {
sr.dropped++
continue
}
sr.record(m)
sr.received++
}
}
}
func (sr *StatsReceiver) record(m *Metric) {
sr.mu.Lock()
defer sr.mu.Unlock()
switch m.Type {
case "c":
sr.counters[m.Name] += m.Value
case "g":
sr.gauges[m.Name] = m.Value
case "ms":
sr.timers[m.Name] = append(sr.timers[m.Name], m.Value)
}
}
func (sr *StatsReceiver) Summary() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
sr.mu.Lock()
fmt.Printf("=== Stats (received=%d dropped=%d) ===\n", sr.received, sr.dropped)
for k, v := range sr.counters {
fmt.Printf(" counter %s = %.0f\n", k, v)
}
for k, v := range sr.gauges {
fmt.Printf(" gauge %s = %.2f\n", k, v)
}
sr.mu.Unlock()
}
}
func main() {
recv, err := NewStatsReceiver(":8125")
if err != nil {
fmt.Println("ERROR:", err)
return
}
fmt.Println("statsd receiver on UDP :8125")
go recv.Summary()
recv.Receive()
}
Send test metrics:
# Send a counter
echo -n "proxy.requests.total:1|c" | nc -u -w1 127.0.0.1 8125
# Send a gauge
echo -n "proxy.connections.active:42|g" | nc -u -w1 127.0.0.1 8125
# Send a timer
echo -n "proxy.request.latency:45.3|ms" | nc -u -w1 127.0.0.1 8125
Real-World Context
DDoS amplification via UDP: attacker sends 100-byte DNS query with spoofed source IP → DNS server sends 7000-byte response to victim. Your security stack defends against this by rate limiting UDP responses per source IP (Day 98). Understanding the attack vector is why you're building this from scratch.