Frontend Labs
⋯

Day 94 — SYN Flood Defense

Learning objective Detect and limit half-open TCP connections per source IP
Key packages net, sync, sync/atomic, time
Security context SYN floods exhaust server connection tables; defense requires limiting half-open state per IP

Key Concepts

SYN flood mechanics:

  1. Attacker sends SYN packets with spoofed source IPs
  2. Server allocates connection state, sends SYN-ACK
  3. Attacker never sends ACK — server waits (default: 75s on Linux)
  4. Server's half-open queue fills; legitimate connections rejected

Go-side: The OS handles SYN cookies. Your Go code limits connections after Accept returns — so you're limiting fully-established connections that connected but haven't sent data yet.

Exercise — Half-Open Connection Limiter

package ddos

import (
    "net"
    "sync"
    "sync/atomic"
    "time"
)

// HalfOpenTracker tracks connections that connected but haven't sent data
type HalfOpenTracker struct {
    mu          sync.Mutex
    perIP       map[string]int // count of half-open conns per IP
    maxPerIP    int
    drainAfter  time.Duration
    totalHalf   atomic.Int64
    totalBlocked atomic.Int64
}

func NewHalfOpenTracker(maxPerIP int, drainAfter time.Duration) *HalfOpenTracker {
    return &HalfOpenTracker{
        perIP:      make(map[string]int),
        maxPerIP:   maxPerIP,
        drainAfter: drainAfter,
    }
}

// TrackConn returns a monitored connection.
// Call this immediately after net.Listener.Accept().
// If the IP has too many half-open conns, returns false — caller should close conn.
func (t *HalfOpenTracker) TrackConn(conn net.Conn) (net.Conn, bool) {
    host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
    if err != nil {
        return conn, true // can't parse — let through
    }

    t.mu.Lock()
    count := t.perIP[host]
    if count >= t.maxPerIP {
        t.mu.Unlock()
        t.totalBlocked.Add(1)
        return conn, false
    }
    t.perIP[host] = count + 1
    t.mu.Unlock()
    t.totalHalf.Add(1)

    // Wrap the connection; when data arrives, remove from half-open
    return &trackedConn{
        Conn:    conn,
        tracker: t,
        ip:      host,
        timer:   time.AfterFunc(t.drainAfter, func() { t.release(host) }),
    }, true
}

func (t *HalfOpenTracker) release(ip string) {
    t.mu.Lock()
    defer t.mu.Unlock()
    if t.perIP[ip] > 0 {
        t.perIP[ip]--
        if t.perIP[ip] == 0 {
            delete(t.perIP, ip)
        }
    }
    t.totalHalf.Add(-1)
}

func (t *HalfOpenTracker) Stats() (half, blocked int64) {
    return t.totalHalf.Load(), t.totalBlocked.Load()
}

type trackedConn struct {
    net.Conn
    tracker  *HalfOpenTracker
    ip       string
    timer    *time.Timer
    released sync.Once
}

// Read — first data received, connection is no longer half-open
func (tc *trackedConn) Read(b []byte) (int, error) {
    tc.released.Do(func() {
        tc.timer.Stop()
        tc.tracker.release(tc.ip)
    })
    return tc.Conn.Read(b)
}

func (tc *trackedConn) Close() error {
    tc.released.Do(func() {
        tc.timer.Stop()
        tc.tracker.release(tc.ip)
    })
    return tc.Conn.Close()
}

// ProtectedListener wraps net.Listener with SYN flood defense
type ProtectedListener struct {
    net.Listener
    tracker *HalfOpenTracker
}

func NewProtectedListener(l net.Listener, maxHalfOpenPerIP int) *ProtectedListener {
    return &ProtectedListener{
        Listener: l,
        tracker:  NewHalfOpenTracker(maxHalfOpenPerIP, 3*time.Second),
    }
}

func (pl *ProtectedListener) Accept() (net.Conn, error) {
    for {
        conn, err := pl.Listener.Accept()
        if err != nil {
            return nil, err
        }
        tracked, ok := pl.tracker.TrackConn(conn)
        if !ok {
            conn.Close()
            continue // silently drop — don't return error
        }
        return tracked, nil
    }
}

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