Day 95 — Slowloris Detection & Defense
|
|
| Learning objective |
Detect connections dripping headers below a byte-rate threshold |
| Key packages |
net, net/http, sync, sync/atomic, time |
| Security context |
Slowloris exhausts goroutine pools by holding connections indefinitely |
Exercise — Byte-Rate Connection Monitor
package ddos
import (
"net"
"sync"
"sync/atomic"
"time"
)
// ByteRateConn wraps net.Conn and tracks bytes received per second
type ByteRateConn struct {
net.Conn
mu sync.Mutex
bytesInWindow int
windowStart time.Time
windowSize time.Duration
// Slowloris detection thresholds
minBytesPerSec int
graceDeadline time.Time // don't check until this time passes
slowloris atomic.Bool
}
func NewByteRateConn(conn net.Conn, minBytesPerSec int, gracePeriod time.Duration) *ByteRateConn {
return &ByteRateConn{
Conn: conn,
windowStart: time.Now(),
windowSize: time.Second,
minBytesPerSec: minBytesPerSec,
graceDeadline: time.Now().Add(gracePeriod),
}
}
func (c *ByteRateConn) Read(b []byte) (int, error) {
n, err := c.Conn.Read(b)
if n > 0 {
c.mu.Lock()
now := time.Now()
if now.Sub(c.windowStart) >= c.windowSize {
// Check rate for completed window
if now.After(c.graceDeadline) {
rate := c.bytesInWindow // bytes in past second
if rate < c.minBytesPerSec {
c.slowloris.Store(true)
}
}
c.bytesInWindow = n
c.windowStart = now
} else {
c.bytesInWindow += n
}
c.mu.Unlock()
}
return n, err
}
func (c *ByteRateConn) IsSlowloris() bool { return c.slowloris.Load() }
// SlowlorisDetector wraps a listener, monitors byte rates, and closes suspicious conns
type SlowlorisDetector struct {
net.Listener
minBytesPerSec int
gracePeriod time.Duration
checkInterval time.Duration
blockList *ExpirableBlockList // reuse from Day 97
mu sync.Mutex
conns map[*ByteRateConn]struct{}
done chan struct{}
detected atomic.Int64
}
func NewSlowlorisDetector(l net.Listener, minBytesPerSec int) *SlowlorisDetector {
sd := &SlowlorisDetector{
Listener: l,
minBytesPerSec: minBytesPerSec,
gracePeriod: 5 * time.Second,
checkInterval: 2 * time.Second,
conns: make(map[*ByteRateConn]struct{}),
done: make(chan struct{}),
}
go sd.monitor()
return sd
}
func (sd *SlowlorisDetector) Accept() (net.Conn, error) {
conn, err := sd.Listener.Accept()
if err != nil {
return nil, err
}
monitored := NewByteRateConn(conn, sd.minBytesPerSec, sd.gracePeriod)
sd.mu.Lock()
sd.conns[monitored] = struct{}{}
sd.mu.Unlock()
return monitored, nil
}
func (sd *SlowlorisDetector) monitor() {
ticker := time.NewTicker(sd.checkInterval)
defer ticker.Stop()
for {
select {
case <-sd.done:
return
case <-ticker.C:
sd.mu.Lock()
for c := range sd.conns {
if c.IsSlowloris() {
sd.detected.Add(1)
ip, _, _ := net.SplitHostPort(c.RemoteAddr().String())
_ = ip // add to block list in production
c.Close()
delete(sd.conns, c)
}
}
sd.mu.Unlock()
}
}
}
func (sd *SlowlorisDetector) removeConn(c *ByteRateConn) {
sd.mu.Lock()
delete(sd.conns, c)
sd.mu.Unlock()
}