nfqcooldown is an NFQUEUE-based IPv4 TCP SYN rate-limiting and shaping daemon for Linux.
It applies configurable rate-limiting and shaping policies to incoming TCP SYN packets.
The project is intended for experiments with TCP connection pacing, firewall behavior, packet filtering strategies, and client connection patterns.
nfqcooldown receives selected packets from the Linux kernel through Netfilter NFQUEUE.
For each incoming TCP SYN packet, it tracks the source IP address and applies a cooldown policy. If a client tries to open a new TCP connection too soon, nfqcooldown can:
- temporarily delay a SYN packet to smooth bursts of new connections
- drop the SYN packet
- mark the SYN packet so nftables can actively reject it
This makes it possible to implement packet handling logic that is difficult or impossible to express with standard iptables modules such as recent, hashlimit, or connlimit.
- Per-source-IP TCP SYN rate limiting and shaping
- NFQUEUE-based userspace packet decisions
- SYN burst shaping with bounded packet delays
- Packet-mark bypass support
- Packet marking for nftables-based active rejection
- DROP action with fixed, random, and jitter cooldown modes
- IP/CIDR whitelist
- Verbose per-packet logging
- Periodic aggregate statistics
- systemd-friendly design
- Written in Go
curl -fsSL https://raw.githubusercontent.com/zperetz/nfqcooldown/main/install.sh | sudo bashA typical setup looks like this:
incoming TCP SYN
|
v
iptables NFQUEUE rule
|
v
nfqcooldown daemon
|
+--> ACCEPT
+--> DROP
+--> SHAPE
+--> MARK FOR REJECT
Example iptables rule:
sudo iptables -t mangle -I INPUT 1 -i eth0 -p tcp --dport 443 \
-m tcp --tcp-flags SYN,RST,ACK SYN \
-j NFQUEUE --queue-num 443 --queue-bypassThe --queue-bypass option is recommended. If the daemon is stopped or crashes, packets continue through the normal firewall path instead of being blocked.
If you use the default filter INPUT chain, make sure the normal ACCEPT rule remains below the NFQUEUE rule:
sudo iptables -A INPUT -i eth0 -p tcp --dport 443 -j ACCEPTFor a more robust setup, nftables is recommended. The following example also includes the iOS TCP fingerprint bypass for telemt. The example uses two packet-mark bits:
0x400— bypasses nfqcooldown processing for matching Telemt iOS packets0x800— marks packets that nfqcooldown wants nftables to reject
sudo mkdir -p /etc/nftables.d
sudo tee /etc/nftables.d/nfqcooldown.nft >/dev/null <<'EOF'
table ip nfqcooldown {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
@th,108,20 0x2ffff \
@th,160,16 0x0204 \
@th,192,16 0x0103 \
@th,224,24 0x010108 \
@th,320,32 0x04020000 \
meta mark set meta mark | 0x400 \
counter
}
chain input {
type filter hook input priority mangle; policy accept;
iifname "eth0" \
tcp dport 443 \
tcp flags & (fin|syn|rst|ack) == syn \
queue flags bypass to 443
}
chain input_reject {
type filter hook input priority mangle + 1; policy accept;
iifname "eth0" \
tcp dport 443 \
tcp flags & (fin|syn|rst|ack) == syn \
meta mark & 0x800 != 0 \
counter \
reject with icmp type host-unreachable
}
}
EOF
sudo tee /etc/systemd/system/nfqcooldown-nft.service >/dev/null <<'EOF'
[Unit]
Description=Load nfqcooldown nftables rules
After=network-online.target ufw.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStartPre=-/usr/sbin/nft delete table ip nfqcooldown
ExecStart=/usr/sbin/nft -f /etc/nftables.d/nfqcooldown.nft
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOFCheck and apply your nftables rules:
#Check
sudo nft -c -f /etc/nftables.d/nfqcooldown.nft
#Apply
sudo systemctl daemon-reload
sudo systemctl enable --now nfqcooldown-nft.serviceDo not forget to replace eth0 with your external interface.
Check your nft rules status:
sudo systemctl status nfqcooldown-nft.service
sudo nft list table ip nfqcooldownBuild the binary:
make buildInstall it manually:
sudo install -m 0755 ./bin/nfqcooldown /usr/local/sbin/nfqcooldownOr use:
sudo make installAdd an iptables NFQUEUE rule:
sudo iptables -I INPUT 1 -i eth0 -p tcp --dport 443 \
-m tcp --tcp-flags SYN,RST,ACK SYN \
-j NFQUEUE --queue-num 443 --queue-bypassStart nfqcooldown:
sudo nfqcooldown \
--queue 443 \
--packet syn \
--action shape \
--burst-interval 1050ms \
--burst-max-delay 350ms \
--max-pending-delays 4 \
--cleanup-every 8s \
--cleanup-after 15s \
--skip-mark 0x400Other usage examples:
Allow one SYN per source IP every 500 ms:
sudo nfqcooldown \
--queue 443 \
--action drop \
--mode fixed \
--cooldown 500msChoose a random cooldown between 300 ms and 450 ms after each accepted SYN:
sudo nfqcooldown \
--queue 443 \
--action drop \
--mode random \
--min-delay 300ms \
--max-delay 450msUse a 500 ms base cooldown with ±100 ms jitter:
sudo nfqcooldown \
--queue 443 \
--action drop \
--mode jitter \
--cooldown 500ms \
--jitter 100mssudo nfqcooldown \
--queue 443 \
--action drop \
--mode fixed \
--cooldown 500ms \
--whitelist 90.80.70.60,185.186.187.188/32sudo nfqcooldown \
--queue 443 \
--packet syn \
--action shape \
--verboseVerbose mode logs individual packet decisions.
| Option | Description |
|---|---|
--queue |
NFQUEUE number |
--packet |
Packet type; synack is intended for diagnostics |
--action |
Action inside cooldown: drop or shape |
--mode |
Cooldown algorithm: fixed, random, or jitter |
--cooldown |
Base cooldown duration for fixed or jitter mode |
--min-delay |
Minimum delay for random mode |
--max-delay |
Maximum delay for random mode |
--jitter |
Jitter range around base cooldown |
--whitelist |
Comma-separated list of trusted IPs or CIDRs (IPv4) |
--verbose |
Enable per-packet decision logging |
--seed |
Random seed for reproducible experiments |
--stats-every |
Aggregate statistics interval |
--cleanup-every |
Remove inactive IPs from memory every duration |
--cleanup-after |
Remove inactive IPs from memory after duration |
--forget-on-drop |
Remove IP in case of DROP action |
--max-drops-per-ip |
Force accept after N consecutive drops from same IP |
--burst-interval |
Min interval between released SYN packets for shape |
--burst-max-delay |
Maximum permitted delay for shape |
--max-pending-delays |
Global pending shaped-packet limit |
--skip-mark |
Accept matching packet marks before shaping/drop |
--reject-mark |
Mark packet for icmp-reject instead of drop (nftables) |
Service file /etc/systemd/system/nfqcooldown.service:
[Unit]
Description=NFQUEUE SYN cooldown
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=/etc/default/nfqcooldown.cfg
ExecStart=/usr/local/sbin/nfqcooldown $ARGS
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.targetExample (best practice, but ymmv)/etc/default/nfqcooldown.cfg:
ARGS="
--queue 443
--packet syn
--action shape
--burst-interval 1050ms
--burst-max-delay 350ms
--max-pending-delays 4
--cleanup-every 8s
--cleanup-after 15s
--skip-mark 0x400
--reject-mark 0x800
"Enable service:
sudo systemctl daemon-reload
sudo systemctl enable --now nfqcooldownView logs:
journalctl -u nfqcooldown -fNormal mode prints aggregate statistics:
[nfqcooldown] accepted=51 delayed=7 dropped=3 tracked_ips=4 action=shape packet=syn burst_interval=1s burst_max_delay=50ms max_pending_delays=2 last=SHAPE-RELEASE ip=1.2.3.4 packet_id=51 remaining=0s
Verbose mode prints every packet decision:
[nfqcooldown] SHAPE-ACCEPT ip=1.2.3.4 packet=15
[nfqcooldown] SHAPE-DELAY ip=1.2.3.4 packet=16 delay=42ms pending=1
[nfqcooldown] SHAPE-RELEASE ip=1.2.3.4 packet=16
- IPv4 only (IPv6 is not supported)
- TCP SYN and SYN/ACK packet parsing
MIT