A small XDP/eBPF SYN-flood guard for a single destination IPv4 + TCP port.
It counts SYN packets per source IP in a sliding time window and blacklists sources that exceed a threshold.
- Attaches an XDP program to an interface (DRV or SKB mode).
- Filters only traffic matching:
- destination IPv4 =
--addr - destination TCP port =
--dest-port
- destination IPv4 =
- Detects SYN && !ACK bursts per source IP.
- When the per-window counter reaches
syn_threshold, the source IP is added toBLACKLISTand subsequent packets are dropped immediately. - Optionally tracks lightweight counters via a Per-CPU stats map.
workspace: this repo is a Cargo workspace with three crates and one helper script.
.
βββ syn-guard/ # userspace loader / CLI (attaches XDP, writes CONFIG, prints stats)
β βββ src/main.rs
βββ syn-guard-ebpf/ # eBPF/XDP program (no_std)
β βββ src/main.rs
β βββ src/packet.rs
β βββ src/stats.rs
βββ syn-guard-common/ # shared structs (Config, Counters, SynStats)
β βββ src/lib.rs
βββ show_blacklist.py # helper: dump BLACKLIST map via bpftool
- Rust (userspace)
- Rust
no_stdeBPF program via Aya (XDP)
- Linux with eBPF/XDP support (VMs are OK; veth works best with SKB mode)
ip(iproute2),bpftool,tcpdump- For generating traffic:
hping3(or alternatives) - Rust toolchain compatible with Aya eBPF builds
- Sufficient
RLIMIT_MEMLOCK(the program sets it to infinity)
-
syn-guard(userspace) features:ebpf-statsβ builds the eBPF object with stats enabled
-
syn-guard-ebpf(eBPF) features:statsβ enables theSTATSPerCpuArray map and counter increments
How it wires together: syn-guardβs build script checks CARGO_FEATURE_EBPF_STATS and passes stats into the syn-guard-ebpf build.
- Userspace
syn-guardloads the compiled eBPF object (syn-guard-ebpf). - It writes runtime configuration into
CONFIG[0](dst IPv4, dst port, window length, threshold). - It attaches the XDP program
syn_guardto the interface that has the--addrIPv4. - The XDP program inspects packets:
- Non-IPv4 / non-TCP / other dst IP/port => PASS
- Fragmented IP packets => PASS
- Already in
BLACKLIST=> DROP SYN && !ACK=> update per-src counters inSYN_STATS- If threshold reached => insert to
BLACKLIST, incrementbanned, DROP
- Optional: userspace periodically reads
STATS(per-CPU) and prints deltas.
Responsibilities:
- Discover interface by IPv4 (
--addr) - Raise memlock rlimit
- Load eBPF bytecode
- Initialize Aya logger (optional)
- Populate
CONFIGmap - Attach/detach XDP program
- Poll
STATSand print deltas
Responsibilities:
- Parse Ethernet/IPv4/TCP headers safely (bounds checks)
- Apply filtering logic for a single dst IP/port
- Maintain maps:
BLACKLIST: src IP β marker byteSYN_STATS: src IP β sliding-window counters (LRU to limit memory)STATS(optional): global counters
Responsibilities:
- Define
#[repr(C)]POD structs shared between user and eBPF:Config,SynStats,Counters
- Key:
u32index (only0is used) - Value:
Config - Purpose: runtime configuration set by userspace once at startup
- Key: source IPv4 in network-order bytes
- Value:
u8marker (usually1) - Invariant: if key exists β source is banned; packets are dropped early
- Key: source IPv4 in network-order bytes
- Value:
SynStats { window_start_ns, syn_count } - Behavior: LRU eviction keeps memory bounded under high-cardinality attacks
- Index:
0 - Value:
Countersper CPU - Read model: userspace sums per-CPU values and prints deltas
syn-guard --addr <IPv4> --dest-port <PORT> [--xdp-mode auto|drv|skb] [--stats-interval SECONDS]Arguments:
--addr <IPv4>: destination IPv4 to protect (also used to find the interface)--dest-port <u16>: destination TCP port to protect--xdp-mode <auto|drv|skb>:auto: try DRV first, fallback to SKBdrv: driver/native XDP (fastest, needs NIC support)skb: generic XDP (best for veth/VMs)
--stats-interval <u64>: how often to print stats deltas (seconds)
sequenceDiagram
autonumber
participant U as User
participant S as syn-guard (userspace)
participant X as XDP prog (syn_guard)
participant M as BPF maps
U->>S: start with --addr/--dest-port
S->>M: write CONFIG[0]
S->>X: load + attach to iface (XDP)
loop each packet
X->>X: parse Eth/IPv4/TCP (bounds checks)
alt not target dst ip/port or not TCP
X-->>S: XDP_PASS
else blacklisted
X->>M: lookup BLACKLIST[src]
X-->>S: XDP_DROP
else SYN && !ACK
X->>M: update SYN_STATS[src]
alt threshold reached
X->>M: insert BLACKLIST[src]
X-->>S: XDP_DROP
else
X-->>S: XDP_PASS
end
end
end
loop every stats interval
S->>M: read STATS (per-CPU)
S-->>U: print deltas
end
sudo apt-get update
sudo apt-get install -y iproute2 bpftool tcpdump hping3 python3Without stats (minimal):
cargo build -p syn-guard --releaseWith stats enabled:
cargo build -p syn-guard --release --features ebpf-statsNote: enabling stats is done via userspace feature
ebpf-stats, notstats.
These commands must be run as root:
# Clean leftovers (safe to run even if they don't exist)
sudo ip netns del nsA 2>/dev/null || true
sudo ip netns del nsB 2>/dev/null || true
sudo ip link del vethA 2>/dev/null || true
# Create namespaces
sudo ip netns add nsA
sudo ip netns add nsB
# Create veth pair
sudo ip link add vethA type veth peer name vethB
# Move interfaces into namespaces
sudo ip link set vethA netns nsA
sudo ip link set vethB netns nsB
# Assign IPs
sudo ip netns exec nsA ip addr add 192.168.100.1/24 dev vethA
sudo ip netns exec nsB ip addr add 192.168.100.2/24 dev vethB
# Bring links up
sudo ip netns exec nsA ip link set dev vethA up
sudo ip netns exec nsB ip link set dev vethB upsudo ip netns exec nsB tcpdump -i vethB -nn -v ipsudo ip netns exec nsB /home/ubuntu/syn-guard/target/release/syn-guard \
--addr 192.168.100.2 --dest-port 5555 --xdp-mode skb --stats-interval 1Expected output (example):
Attached XDP (SKB_MODE) to vethB. Filtering dst 192.168.100.2:5555
stats(+1s): pass=... drop=... syn_seen=... banned=... bl_hits=... parse_err=...
Full flood (very aggressive):
sudo ip netns exec nsA hping3 -S --flood -p 5555 192.168.100.2 -I vethASafer bounded burst (recommended for debugging):
sudo ip netns exec nsA hping3 -S -c 260 -i u2000 -p 5555 192.168.100.2 -I vethAsudo python3 show_blacklist.py --netns nsB --onceExample:
[12:45:27] BLACKLIST size=1
192.168.100.1
sudo python3 show_blacklist.py --netns nsB --interval 1.0sudo python3 show_blacklist.py --netns nsB --diff --interval 1.0--netns <name>: run bpftool inside a network namespace (ip netns exec)--map <name>: map name to dump (defaultBLACKLIST)--once: dump once and exit--interval <sec>: polling interval--diff: print only newly added IPs since previous poll