Frontend Labs
⋯

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()
}

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