
Script to implement Q-Feeds directly on NFtables or IPtables
Automated malware IP blocklist for Linux servers — supports nftables and iptables+ipset
Obtain a free API key at tip.qfeeds.com.
git clone https://github.com/Q-Feeds/NFtables-IPtables-integration-script.git
cd NFtables-IPtables-integration-script
chmod +x qfeeds-installer.sh qfeeds-uninstaller.sh
sudo ./qfeeds-installer.sh
The installer will:
Your server is now protected. The cron job checks for updates every 20 minutes (configurable), and actual API calls only happen when your license allows.
This solution periodically downloads the latest threat intelligence feed from Q-Feeds and applies it as firewall rules, allowing you to:
The installer automatically detects which firewall backend is available:
| Priority | Detection | Backend |
|---|
The detected backend is stored in the configuration file. The updater and uninstaller scripts use it to run the correct firewall commands.
Both backends use the same split-set strategy for maximum performance:
nftables backend:
┌─────────────────────────────────────────────────────────┐
│ table ip qfeeds │
│ │
│ ┌─────────────────────────┐ ┌───────────────────────┐ │
│ │ qfeeds_blacklist_v4 │ │ qfeeds_blacklist_v4 │ │
│ │ (hash set) │ │ _nets (interval set) │ │
│ │ │ │ │ │
│ │ Individual IPs │ │ CIDR ranges │ │
│ │ ~99% of entries │ │ ~1% of entries │ │
│ │ O(1) lookup & insert │ │ O(log n) lookup │ │
│ └─────────────────────────┘ └───────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ qfeeds_whitelist_v4 │ │
│ │ (interval set) │ │
│ │ Your allowed IPs/CIDRs │ │
│ └─────────────────────────┘ │
│ │
│ chain input-chain (hook input, priority 0, accept) │
│ → ip saddr @qfeeds_whitelist_v4 accept │
│ → ip saddr @qfeeds_blacklist_v4 drop │
│ → ip saddr @qfeeds_blacklist_v4_nets drop │
│ │
│ chain output-chain (if enabled) │
│ → ip daddr @qfeeds_whitelist_v4 accept │
│ → ip daddr @qfeeds_blacklist_v4 drop │
│ → ip daddr @qfeeds_blacklist_v4_nets drop │
└─────────────────────────────────────────────────────────┘
iptables+ipset backend:
┌──────────────────────────────────────────────────────────┐
│ ipset sets │
│ │
│ ┌─────────────────────────┐ ┌────────────────────────┐ │
│ │ qfeeds_blacklist_v4 │ │ qfeeds_blacklist_v4 │ │
│ │ (hash:ip) │ │ _nets (hash:net) │ │
│ │ maxelem 1000000 │ │ maxelem 65536 │ │
│ │ │ │ │ │
│ │ Individual IPs │ │ CIDR ranges │ │
│ └─────────────────────────┘ └────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ qfeeds_whitelist_v4 │ │
│ │ (hash:net) │ │
│ └─────────────────────────┘ │
│ │
│ iptables: INPUT/OUTPUT jump to a dedicated chain │
│ (jump rule tagged -m comment "qfeeds"): │
│ │
│ chain QFEEDS_INPUT (rebuilt each run, in order): │
│ -m set --match-set whitelist_v4 src -j ACCEPT │
│ -m set --match-set blacklist_v4 src -j DROP │
│ -m set --match-set blacklist_v4_nets src -j DROP │
│ (QFEEDS_OUTPUT mirrors this with dst, if enabled) │
└──────────────────────────────────────────────────────────┘
The same structure exists for IPv6 (ip6 qfeeds table or ip6tables + family inet6 ipsets).
Why two set types?
┌──────────────────────────────────────────────────────┐
│ 1. Check license schedule (licenses.php API) │
│ → Skip run if not yet time for next update │
│ 2. Determine sync mode (full or diff) │
│ 3. Fetch IPv4 feed (ipv6=0) and IPv6 feed │
│ (ipv6=only) separately │
│ 4. Separate IPs from CIDRs in awk │
│ 5. Batch-load into hash set (IPs) and net/interval │
│ set (CIDRs) │
│ 6. Update whitelist sets from config │
│ 7. Persist rules │
└──────────────────────────────────────────────────────┘
The diff sync is per API key — the API tracks your last successful pull and only returns changes since then. If a diff fails, the script automatically falls back to a full sync.
Self-healing: Diff updates only patch the existing set. If that set is ever lost or truncated — for example a reboot where the firewall rules were not persisted, a manual flush, or a previous partial sync — the updater detects the missing baseline (live element count is 0 or far below the last recorded count) and forces a full rebuild instead of diffing onto an empty set. As an extra safety net it also forces a periodic full sync (every 24h by default, via
FULL_SYNC_MAX_AGE).
The updater checks the Q-Feeds license API (licenses.php) before every run. If your license's next_update timestamp hasn't been reached yet, the script exits early without making unnecessary API calls. The cron job runs frequently (default: every 20 minutes), but actual updates only happen when your license allows.
Before installing, ensure you have:
api.qfeeds.comThe installer will automatically install required dependencies:
nftables, curl, jq, util-linuxiptables, ipset, curl, jq, util-linuxVisit tip.qfeeds.com to obtain your free Q-Feeds API token.
git clone https://github.com/Q-Feeds/NFtables-IPtables-integration-script.git
cd NFtables-IPtables-integration-script
chmod +x qfeeds-installer.sh qfeeds-uninstaller.sh
sudo ./qfeeds-installer.sh
The installer will ask the following questions:
Enter your Q-Feeds API Token:
Your token from tip.qfeeds.com. The installer refuses to continue if empty.
Enter feed type [default: malware_ip]:
Default is malware_ip. Only change this if Q-Feeds has provided you with a different feed type.
Enter the limit of IPs to fetch (leave empty for no limit):
Press Enter for no limit (recommended). Enter a number to cap the feed size.
Block INCOMING connections from malicious IPs? [Y/n]:
Block OUTGOING connections to malicious IPs? [y/N]:
Configure a whitelist of IPs/CIDRs that must NEVER be blocked? [y/N]:
Enter IPv4 whitelist (comma-separated, e.g. 1.2.3.4,5.6.7.8):
Enter IPv6 whitelist (comma-separated, e.g. 2001:db8::1):
Add your management IP(s) here to ensure you are never locked out, even if they appear in the feed. Whitelist rules are always checked before blacklist rules.
Enter cron schedule (e.g., '*/20 * * * *') [default: */20 * * * *]:
How often the updater checks for new data. Default is every 20 minutes. The license-based scheduling ensures the API is only called when your license allows an update.
Re-running the installer with a customized crontab: If a Q-Feeds cron entry already exists, the installer asks before touching it:
An existing Q-Feeds cron entry was found in the current crontab. Replace it with a fresh default entry? Choosing 'no' keeps your crontab unchanged [y/N]:Answer
no(the default) to keep your existing crontab. A clean first install has no existing entry and skips this prompt. For unattended installs, setQFEEDS_SKIP_CRON=1to leave the crontab untouched without prompting.
All settings are stored in /etc/qfeeds/qfeeds_config.conf. You can edit this file directly without rerunning the installer. Changes take effect on the next cron run.
# Show the table structure and rules
nft list table ip qfeeds
# Count loaded IPv4 IPs (individual addresses)
nft list set ip qfeeds qfeeds_blacklist_v4 | grep -oP '\d+\.\d+\.\d+\.\d+' | wc -l
# Show loaded CIDR ranges
nft list set ip qfeeds qfeeds_blacklist_v4_nets | head -20
# Count loaded IPv6 addresses
nft list set ip6 qfeeds qfeeds_blacklist_v6 | wc -l
# List all Q-Feeds ipsets and their sizes
ipset list -t | grep -A4 qfeeds
# Count loaded IPv4 IPs
ipset list qfeeds_blacklist_v4 | tail -n +9 | wc -l
# Show loaded CIDR ranges
ipset list qfeeds_blacklist_v4_nets | tail -n +9 | head -20
# Show the qfeeds jump rule in INPUT, then the dedicated chain's block rules
iptables -L INPUT -n --line-numbers | grep qfeeds
iptables -L QFEEDS_INPUT -n
ip6tables -L QFEEDS_INPUT -n
# Check the log (last 20 entries)
tail -20 /var/log/qfeeds_blocklist.log
# Check for errors
grep -i "error" /var/log/qfeeds_blocklist.log
# Normal run (respects license schedule)
sudo /usr/local/bin/update_qfeeds_blocklist.sh
# Force a full sync (ignores schedule, reloads everything)
sudo QFEEDS_FORCE_UPDATE=1 /usr/local/bin/update_qfeeds_blocklist.sh
# Verify cron is set up
sudo crontab -l | grep qfeeds
Installation fails with "Unable to locate package"
curl, jq, util-linux (for flock), plus nftables or iptables+ipset.Sets are empty after installation
tail -50 /var/log/qfeeds_blocklist.logsudo QFEEDS_FORCE_UPDATE=1 /usr/local/bin/update_qfeeds_blocklist.sh"Not time yet. Next update scheduled at..."
licenses.php index and uses the cached next_update as its schedule gate. After a successful pull it refreshes that local index for the next cycle.Rules don't persist after reboot
netfilter-persistent is installed, rules are saved automaticallynetfilter-persistent is absent, the updater now writes the full ruleset to /etc/nftables.conf automatically and enables the nftables service; you can still save manually with nft list ruleset > /etc/nftables.confipset save > /etc/ipset.conf and iptables-save; you can also save manually"Batch nft -f failed. Falling back to per-command execution..."
wmem_max) is restricted. The per-command fallback works correctly and is fast (~10 seconds for 400k+ IPs).Syntax error: "unexpected string"
ip saddr/ip daddr syntax which requires nftables 0.9+."Error: Could not process rule: Message too long"
"ipset restore failed"
ipset is installed: command -v ipsetgrep -i "error" /var/log/qfeeds_blocklist.loglsmod | grep ip_setiptables rules not showing up
QFEEDS_INPUT / QFEEDS_OUTPUT chains; INPUT/OUTPUT only hold a -j QFEEDS_INPUT jump tagged with the qfeeds commentiptables -L INPUT -n | grep qfeeds (the jump) and iptables -L QFEEDS_INPUT -n (the block rules)-m comment --comment "qfeeds" for identificationxt_set module is loaded: modprobe xt_set"ipset create ... failed"
hash:ip or hash:net types may not be available. Upgrade your kernel or install ipset from a newer repository.sudo ./qfeeds-uninstaller.sh
The uninstaller removes everything based on the detected backend:
nftables backend:
ip qfeeds and ip6 qfeeds tables (including all chains, rules, and sets)iptables backend:
qfeeds comment (including the jump rules)QFEEDS_INPUT / QFEEDS_OUTPUT chainsqfeeds_blacklist_v4, qfeeds_blacklist_v4_nets, qfeeds_whitelist_v4, and IPv6 equivalents)Both backends:
/etc/qfeeds/)/usr/local/bin/update_qfeeds_blocklist.sh)If the config file is missing, the uninstaller tries cleanup for both backends.
Note: The uninstaller does not remove system packages (curl, jq, ipset, etc.) that were installed as dependencies.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Use at your own risk.
Please test these scripts in your environment before deploying them in production. The author is not responsible for any issues or damages that may occur from their use.
AI assistance: Parts of this project (code, fixes, and documentation) were written with the help of AI tools and subsequently reviewed by maintainers. While we test and review changes, please review the scripts yourself before running them and report anything that looks off.
| 1st | nft command found | nftables |
| 2nd | iptables command found | iptables+ipset |
| — | Neither found | Error (exit) |
| Mode | When | What it does |
|---|
| Full sync | First run, forced update, after a diff failure, when the local set has lost its baseline (empty or much smaller than expected), or when the last sync is older than FULL_SYNC_MAX_AGE (default 24h) | Fetches and validates each feed first, then flushes and reloads the blacklist sets. The set is only flushed once valid data is in hand, so a failed fetch never leaves you unprotected |
| Diff sync | Subsequent runs (malware_ip feed only) with a healthy local set | Fetches only additions (+) and removals (-) since last pull |
| Variable | Description | Default |
|---|
BACKEND | Firewall backend (nftables or iptables) | (auto-detected) |
API_TOKEN | Your Q-Feeds API token | (required) |
FEED_TYPE | Feed type to fetch | malware_ip |
LIMIT | Max IPs to fetch (empty = no limit) | (empty) |
BLOCK_INCOMING | Block incoming from blacklisted IPs | yes |
BLOCK_OUTGOING | Block outgoing to blacklisted IPs | no |
WHITELIST_V4 | Comma-separated IPv4 whitelist | (empty) |
WHITELIST_V6 | Comma-separated IPv6 whitelist | (empty) |
LOG_FILE | Path to log file | /var/log/qfeeds_blocklist.log |
FULL_SYNC_MAX_AGE | Max age in seconds before a full re-sync is forced (defense in depth) | 86400 (24h) |
| Path | Purpose |
|---|
/etc/qfeeds/qfeeds_config.conf | Configuration file |
/etc/qfeeds/.last_sync | State file for full/diff sync tracking |
/etc/qfeeds/.last_count | Last successful element count, used to detect a lost baseline |
/usr/local/bin/update_qfeeds_blocklist.sh | Updater script (runs via cron) |
/var/log/qfeeds_blocklist.log | Log file |