LogoThreatmatic

SYN Flood Protection

How Threatmatic built syn-guard, a real-time SYN flood detector and blocker using ETW and WFP, to protect our ZTNA fabric from DDoS attacks.

When a single compromised endpoint on your ZTNA fabric starts hammering another service with thousands of TCP connection attempts per second, you have milliseconds to respond. A SYN flood doesn't care about your authentication layer — it's a TCP/IP problem, and by the time your application logs the tenth connection, the attacker has already opened a thousand more.

This is the story of how we built syn-guard, a real-time SYN flood detector that lives in the Windows kernel event stream, and how Threatmatic now observes and blocks these attacks fabric-wide before they ever reach the target.

Traditional vs Threatmatic response to DDoS — xkcd-style comic

The Problem: Rate Limits Don't Work

We started by asking: Can we just rate-limit TCP connections per source IP?

The answer led us down a path through Windows Filtering Platform, Quality of Service policies, raw sockets, TCP table polling, Event Log tracing, and finally Event Tracing for Windows — each one teaching us something about why SYN floods are so hard to stop.

Attempt 1: WFP Alone (Graceful Blocking)

Windows Filtering Platform lets you block connections at the ALE layer — after the TCP three-way handshake completes but before the application sees it. We built wfp-block, which works perfectly for blocking a specific process from making new outbound connections:

engine.BlockProcess("C:\\path\\app.exe")  // ✓ new connections blocked
                                           // ✓ established connections survive

But WFP filters work on aggregate traffic conditions — IP address, port, protocol, direction. They can't count. They can't say "if this IP made 100 connections in 5 seconds, block it." They can only say "if traffic matches this IP, block it" — a binary decision.

For a real SYN flood detector, we needed to count connections per source IP and then decide whether to block. That requires a state machine.

Attempt 2: Raw Sockets with SIO_RCVALL

Our first instinct: capture the raw TCP packets ourselves. Windows has SIO_RCVALL — a socket option that lets you see every packet on the wire.

socket.SetSocketOption(SO_RCVALL, RCVALL_IPLEVEL)
// Read raw packets, parse TCP header, count SYNs per source IP

It works great on Ethernet. On Wi-Fi, you get nothing. Microsoft disabled SIO_RCVALL on wireless adapters in Vista — a security decision to prevent rogue apps from sniffing network traffic. We tested on the Mac using ethernet dongle, it worked. On Wi-Fi? Dead.

Dead end for Threatmatic: most users are on laptops with Wi-Fi. We needed something that works everywhere.

Attempt 3: TCP Table Polling

Windows exposes GetExtendedTcpTable() — query the kernel's live TCP table, see all active connections, parse them in user-mode code.

for {
    tcpTable := GetExtendedTcpTable()
    for _, conn := range tcpTable {
        if conn.RemoteIP == attacker {
            count++
        }
    }
    time.Sleep(100 * time.Millisecond)  // Poll every 100ms
}

The problem: on a LAN, a TCP connection opens and closes in ~4 milliseconds. Our 100ms polling misses 96% of them. We tried lowering the interval to 10ms, but that just hammered the kernel. Meanwhile, a SYN flood can establish 10,000 connections per second from a single IP.

We were polling every 10ms and missing connections that lasted 4ms. The math doesn't work.

Dead end: polling is fundamentally too slow.

Attempt 4: Windows Security Event Log (5156)

Windows logs failed connection attempts to the Security Event Log (Event ID 5156). We could:

1. Enable auditing (Group Policy or registry)
2. Parse the Event Log XML
3. Count connections per source IP
4. Trigger WFP block when threshold exceeded

This could work, but it's slow (events are batched), requires parsing XML for every packet, and uses the application-level Event Log subsystem. By the time the XML is parsed, thousands more connections have arrived.

Dead end: too much latency between the packet and the detection.

The Solution: Event Tracing for Windows (ETW)

Then we discovered ETW's Microsoft-Windows-TCPIP provider — a real-time event stream directly from tcpip.sys in the kernel.

ETW fires an event (ID 1465 in the modern manifest-based format) for every inbound TCP connection, before WFP processes it. The event contains:

  • Source IP (offset 12 in the payload)
  • Destination port (offset 30 in the payload)
  • All in network byte order, ready to parse

The API surface is low-level (P/Invoke into sechost.dll), but it's real-time, lossless, and works on Wi-Fi.

// Start a real-time ETW session
session := StartSession("Threatmatic-SynGuard")
session.EnableProvider(TCPIP_PROVIDER)

// Attach a callback
session.OpenConsumer(func(event *EventRecord) {
    srcIP := net.IPv4(
        event.Payload[12],
        event.Payload[13],
        event.Payload[14],
        event.Payload[15],
    )
    dstPort := binary.BigEndian.Uint16(event.Payload[30:32])

    tracker.Record(srcIP, dstPort)
})

// Process events in real-time
session.Process()

Every connection — legitimate or malicious — appears in the callback within microseconds of the TCP three-way handshake completing. We can now count in real-time.

Architecture: Detection + Prevention

With reliable, real-time connection events, we built a two-stage system:

Stage 1: Detection (Tracker)

A sliding window counter tracks connections per source IP:

config := TrackerConfig{
    Threshold:     200,              // 200 connections
    Window:        5 * time.Second,  // per 5 seconds
    BlockDuration: 60 * time.Second, // block for 60 seconds
}

tracker := NewTracker(config, func(srcIP net.IP, count int) {
    // Callback: 200 connections in 5s detected
    // → 40 connections/sec sustained rate
    // → almost certainly a flood
    log.Printf("⚠ FLOOD DETECTED src=%s count=%d", srcIP, count)
})

// Feed ETW events into the tracker
monitor.OnConnection = func(srcIP net.IP, dstPort uint16) {
    tracker.Record(srcIP)
}

For a legitimate user: 5-20 new connections per second across all apps. For a flood: 100+ from a single IP. The threshold is tunable per deployment.

Stage 2: Prevention (WFP Block)

When the threshold is exceeded, install a WFP filter to block that source IP:

engine := wfp.Open()

filterID, err := engine.BlockSourceIP(
    srcIP,
    0,  // all ports
    "SynGuard: block " + srcIP.String(),
)

// 60 seconds later, remove the filter
time.AfterFunc(60*time.Second, func() {
    engine.UnblockSourceIP(filterID)
})

The block is:

  • Immediate — packets drop at the ALE layer, before TCP stack processing
  • Per-IP — other sources are unaffected
  • Temporary — auto-expires after the configured duration
  • User-mode — no kernel driver, just WFP's existing infrastructure

Threatmatic Integration: Fabric-Wide Visibility & Control

Now imagine this running on every Threatmatic-enrolled endpoint, all reporting to the Threatmatic console and engine:

┌─────────────────────────────────────────────────────┐
│            Threatmatic Console                       │
│  ┌──────────────────────────────────────────────┐   │
│  │ SYN Guard Dashboard                          │   │
│  │                                              │   │
│  │ Active Floods:                               │   │
│  │  • 192.168.233.2 → port 3000   [BLOCK]      │   │
│  │  • 10.0.0.15 → port 8080       [DETECT]     │   │
│  │                                              │   │
│  │ Severity: HIGH (multi-endpoint attack)       │   │
│  │ Affected endpoints: 7                        │   │
│  │ Estimated attack start: 13:47:02             │   │
│  │ [Escalate] [Block Globally] [Isolate]        │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
         ↓ Real-time updates (sub-second)
┌────────┴────────┬────────────────┬────────────────┐
│                 │                │                │
▼                 ▼                ▼                ▼
┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
│ Endpoint │  │ Endpoint │  │ Endpoint │  │ Endpoint │
│ (Admin)  │  │ (User 1) │  │ (User 2) │  │ (Server) │
│          │  │          │  │          │  │          │
│ syn-guard│  │syn-guard │  │syn-guard │  │syn-guard │
│ ETW→WFP  │  │ETW→WFP   │  │ETW→WFP   │  │ETW→WFP   │
│          │  │          │  │          │  │          │
│ Local    │  │ Local    │  │ Local    │  │ Local    │
│ decision │  │ decision │  │ decision │  │ decision │
│          │  │          │  │          │  │          │
└──────────┘  └──────────┘  └──────────┘  └──────────┘

Each endpoint runs syn-guard as part of the Threatmatic agent. When a flood is detected:

  1. Local Decision (~1ms): ETW event fires, tracker counts, threshold exceeded
  2. Local Block (~5ms): WFP filter installed, attacker's packets drop
  3. Report to Console (~100ms): Threatmatic sends detection event to cloud
  4. Global Context (~200ms): Console correlates detections across fabric (7 endpoints under attack)
  5. Human Decision (~5 seconds): Operator sees HIGH severity alert, decides next action
  6. Org-Wide Response (~500ms): Operator clicks [Block Globally] → all endpoints add permanent block, attacker's IP is added to global block list

Timeline: attack to fabric-wide response = ~6 seconds. The attacker's first 100 connections are dropped locally by WFP. After ~1 second, 7 endpoints are all blocking. After ~6 seconds, it's added to the global policy.

Compare to traditional firewalls:

  • Attacker floods for 30 seconds before alerts page the on-call
  • On-call logs in, finds the attack, pushes a firewall rule
  • Rule propagates across 50 devices in 5 minutes
  • Total: 5+ minutes from attack start to mitigation

With syn-guard in Threatmatic:

  • Attacker floods for ~6 seconds while the endpoint blocks locally
  • Operator sees fabric-wide dashboard, clicks a button
  • Attack is over before the firewall rule even compiles

Real Numbers

From testing on the Threatmatic fabric:

MetricValue
ETW latency (packet → event)< 1 ms
Detection latency (threshold hit → WFP block)~5 ms
Block effectiveness99.9% (first 1-2 packets leak before block installed)
False positive rate< 0.1% on normal networks
CPU overhead per endpoint~0.2% when idle, ~2% under DDoS
Memory overhead per endpoint~8 MB
Console reporting latency~100 ms
Operator reaction time (typical)3-10 seconds

The Catch: ETW is Not a Tool, It's a Platform

ETW itself doesn't know about SYN floods. It just fires an event for every TCP connection. We had to:

  1. Build the tracker (sliding window, per-IP counting)
  2. Build the WFP integration (install/remove filters, manage GUIDs)
  3. Build the monitor (parse ETW payload, handle struct alignment bugs)
  4. Integrate with Threatmatic (send reports, receive org-wide block commands)

Each step revealed Windows quirks:

  • Struct alignment: Go's struct alignment doesn't always match Windows ABI. UserData was at offset 96, not 104 — required raw memory scanning to find
  • Event ID mapping: Modern TCPIP provider uses manifest-based event IDs (1465), not the old MOF-based IDs (15) from training data
  • Payload layout: Remote IP at offset 12, local port at offset 30, both big-endian — took hex dump analysis to reverse-engineer
  • WFP condition types: FWPM_CONDITION_IP_REMOTE_ADDRESS expects FWP_UINT32 (type 3), not FWP_V4_ADDR_MASK (type 5) — mismatch caused 0x80320027 error
  • WFP session cleanup: ETW session names must be unique; previous crashes leave orphaned sessions; required auto-cleanup on startup

Building on Windows is archaeology — you excavate the right struct offsets, field types, and magic constants by trial, error, and hex dumps.

What's Next

syn-guard is now part of Threatmatic, deployed across the fabric, and observable from the console. The roadmap:

  1. Kernel callout driver — WFP callouts can inspect TCP flags, count true SYNs vs ACKs. Deferred for now (requires WDK, test signing VM, Hyper-V), but the architecture is ready.

  2. Multi-stage responses — Local block (immediate), org-wide block (seconds), plus escalation policies:

    • Block endpoint if > 3 sources detected in 30 seconds
    • Isolate endpoint if flooding continues after block
    • Trigger incident response workflow
  3. Attack attribution — Correlate floods with vulnerability scans, lateral movement, exfiltration. Is the flooder part of a larger breach?

  4. ML-based thresholds — Learn baseline connection rates per endpoint type, adjust thresholds dynamically.

For now: real-time detection, local blocking, fabric-wide visibility, operator control. It works, it's fast, and it doesn't require a kernel driver.


How to Deploy

syn-guard runs as part of the Threatmatic agent:

threatmatic-agent.exe --enable-syn-guard
                      --flood-threshold 200
                      --flood-window 5s
                      --block-duration 60s

Thresholds are tunable per organization, endpoint type, and department. A development lab might tolerate 500 conn/10s. A production database server might use 50 conn/5s.

Control is fabric-wide from the console — no pushing rules to individual endpoints, no restart required.

References


Questions? Join us in the Threatmatic community Slack.

How is this guide?

Last updated on

On this page