THE DEPLOYMENT HANDBOOK / Linux packet filtering

nftables integration guide.

Configure Data-Shield IPv4 blocklists on nftables. Replace dedicated IPv4 sets atomically, preserve existing rules and schedule verified two-hour updates.

IPV4 ONLY
STANDARD + CRITICAL
LINUX / NFTABLESDEDICATED IPV4 SETSATOMIC SET REPLACEMENT

Use dedicated sets without replacing your firewall

This guide targets a maintained Linux system with nftables, Python 3.11+, curl, util-linux flock and systemd. The examples were exercised with nftables 1.1.6 in an isolated user and network namespace on 2026-10-28. Check your distribution's supported kernel and nftables version in staging, including available memory for both complete feeds.

Use the dedicated inet data_shield table only if that name is unused. Keep other rulesets and firewall managers intact. If firewalld, UFW, Docker, a distribution tool or a configuration manager owns your firewall, integrate through its supported persistence mechanism: it may otherwise delete externally created tables during reload.

Both Standard and Critical are exact IPv4 sets, with no prefix aggregation. The example enforces Standard on new incoming connections to the host. The Critical set is available as an alternative policy. Loading both sets does not require two drop rules, and does not block IPv6.

Stage and validate a consistent publication

Standard

113,445 unique IPv4 addresses in this published snapshot.

Standard / HTTPS feed URL
https://raw.githubusercontent.com/duggytuxy/Data-Shield_IPv4_Blocklist/refs/heads/main/prod_data-shield_ipv4_blocklist.txt

Critical

122,242 unique IPv4 addresses in this published snapshot.

Critical / HTTPS feed URL
https://raw.githubusercontent.com/duggytuxy/Data-Shield_IPv4_Blocklist/refs/heads/main/prod_critical_data-shield_ipv4_blocklist.txt

Published 28 Oct 2026, 15:31 UTC. Check live counts and SHA-256.

The Linux staging helper retrieves both complete files at an immutable GitHub commit, verifies every public IPv4, rejects duplicates and unordered records, and compares counts, bytes and SHA-256 with the public snapshot. It checks for publication changes and rejects a snapshot older than six hours. A mismatch leaves the active sets untouched. The helper never invokes nftables.

Linux / download and review the validator
curl --fail --show-error --location --proto '=https' --tlsv1.2 \
  https://data-shield.eu/downloads/data-shield-linux.py \
  --output data-shield-linux.py
python3 data-shield-linux.py --help
# Review the downloaded source before installing it.
sudo install -o root -g root -m 0755 data-shield-linux.py /usr/local/sbin/data-shield-linux.py

Create unhooked sets and apply one transaction

Keep a trusted management path and a configuration backup. First check that the table name is unused with sudo nft list tables. Save this definition to a local file, review it, and load it once using sudo nft --file FILE. It deliberately has no hook and therefore blocks nothing yet. create table fails if the dedicated table already exists.

nftables / initial dedicated sets
create table inet data_shield
add set inet data_shield standard { type ipv4_addr; }
add set inet data_shield critical { type ipv4_addr; }

Create /var/lib/data-shield-nftables with root ownership and mode 0700. Save the updater below to /usr/local/sbin/data-shield-nftables-update, owned by root and mode 0750, then run it once as root.

nftables / private working directory
sudo install -d -o root -g root -m 0700 /var/lib/data-shield-nftables
nftables / verified atomic set update
#!/bin/sh
set -eu
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
umask 077
exec 9>/run/lock/data-shield-nftables.lock
flock -n 9 || exit 75
work=$(mktemp -d /var/lib/data-shield-nftables/run.XXXXXX)
trap 'rm -rf -- "$work"' EXIT
trap 'exit 1' HUP INT TERM
python3 /usr/local/sbin/data-shield-linux.py --output "$work/bundle"
nft --check --file "$work/bundle/sets.nft"
nft --file "$work/bundle/sets.nft"
# Save the successfully applied transaction for the next boot.
install -m 0600 "$work/bundle/sets.nft" "$work/current.nft"
mv -f "$work/current.nft" /var/lib/data-shield-nftables/current.nft
install -m 0600 "$work/bundle/manifest.json" "$work/manifest.json"
mv -f "$work/manifest.json" /var/lib/data-shield-nftables/manifest.json

The generated transaction flushes and repopulates only these two dedicated sets. nft --check checks it without applying changes; the subsequent single nft --file applies the whole batch atomically. There is no empty-set interval between profiles. If a command in that batch fails, the existing kernel sets remain. Do not split the transaction into separate shell commands and never use flush ruleset.

See the official atomic replacement guide, named sets documentation and nft manual. Other administrators can still change the table concurrently; reserve ownership and use the same update lock for your own maintenance.

Compare every loaded address

Before adding a hook, export the dedicated table as JSON. The following check rebuilds the canonical sorted file for each exact-address set and compares its full count and SHA-256 with the saved manifest. Run it as root without an update in progress. Extra, missing or unexpected JSON elements fail validation.

nftables / verify complete loaded membership
sudo nft --json list table inet data_shield > loaded-data-shield.json
sudo python3 - loaded-data-shield.json /var/lib/data-shield-nftables/manifest.json <<'PYVERIFY'
import hashlib, ipaddress, json, sys
loaded = json.load(open(sys.argv[1]))
manifest = json.load(open(sys.argv[2]))
for profile in ('standard', 'critical'):
    matches = [item['set'] for item in loaded['nftables']
               if 'set' in item and item['set'].get('family') == 'inet'
               and item['set'].get('table') == 'data_shield'
               and item['set'].get('name') == profile]
    if len(matches) != 1 or matches[0]['type'] != 'ipv4_addr':
        raise SystemExit('Unexpected set: ' + profile)
    values = [ipaddress.IPv4Address(value) for value in matches[0].get('elem', [])]
    raw = ''.join(str(value) + '\n' for value in sorted(values)).encode()
    expected = manifest['profiles'][profile]
    if (len(set(values)) != len(values) or len(values) != expected['blocked']
            or hashlib.sha256(raw).hexdigest() != expected['sha256']):
        raise SystemExit('Membership mismatch: ' + profile)
    print(profile + ': complete membership and SHA-256 match')
PYVERIFY

Attach a deliberately scoped policy

Replace WAN_INTERFACE with your actual ingress interface and review the hook priority against the existing ruleset. Load this separate file only after verifying the sets. This example blocks new IPv4 connections from Standard members to the local host; it leaves established connections alone. Choose @critical instead when that is the intended policy.

nftables / example host-input policy
add chain inet data_shield ingress { type filter hook input priority -5; policy accept; }
add rule inet data_shield ingress iifname "WAN_INTERFACE" ct state new ip saddr @standard counter drop

For routed traffic, use a reviewed forward hook instead. Port-forwarded services and containers may traverse forwarding rather than local input. Do not add output filtering by accident. The chain documentation explains that a drop is final, while accept in one base chain does not bypass later base chains. An allow rule in another chain therefore does not necessarily exempt traffic from this drop.

For a required local management exception, add a narrowly scoped return rule at the beginning of this dedicated chain, before its drop rule, with the exact source/interface/service constraints you have approved. Returning from this chain does not bypass other firewall chains. Record the exception's expiry and verify the actual packet path. Use counters and a controlled test connection to confirm both intended blocking and unaffected traffic.

Retain the last successful set across reboot

Use your existing firewall configuration mechanism to create the dedicated table, sets and reviewed chain on boot. Include the saved transaction after creating the two sets. For a system managed directly by nftables.service, add this line in the appropriate location of its existing configuration, without replacing the file or its other rules:

nftables / include the last verified members
include "/var/lib/data-shield-nftables/current.nft"

Generate the saved file with a successful updater run before making the include mandatory. Check the complete boot configuration with nft --check --file and test a reboot in staging. Ensure the private state directory is available before firewall restoration. A live kernel update and a disk write cannot be one transaction: a disk error after a successful update can leave an older boot copy, so treat any nonzero updater exit as a failure requiring investigation.

A failed download keeps the previous active and saved sets. This maintains blocking during an outage, but also retains removed addresses until a successful refresh. Monitor the manifest timestamp and alert on the first missed update; do not treat an old set as current simply because it is nonempty.

Schedule the dedicated updater

After initial validation, create these units in /etc/systemd/system/. Adapt After=nftables.service to your actual firewall manager. Keep its own service and rules intact.

nftables / data-shield-nftables.service
[Unit]
Description=Update verified Data-Shield nftables sets
Wants=network-online.target
After=network-online.target nftables.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/data-shield-nftables-update
TimeoutStartSec=15min
UMask=0077
nftables / data-shield-nftables.timer
[Unit]
Description=Refresh Data-Shield nftables sets every two hours

[Timer]
OnBootSec=5min
OnUnitActiveSec=2h
RandomizedDelaySec=2min

[Install]
WantedBy=timers.target
nftables / enable and inspect the dedicated schedule
sudo systemctl daemon-reload
sudo systemctl start data-shield-nftables.service
sudo systemctl enable --now data-shield-nftables.timer
sudo systemctl list-timers data-shield-nftables.timer
sudo journalctl -u data-shield-nftables.service -n 40 --no-pager

Roll back only the dedicated table

Disable the dedicated timer, stop a running updater, and remove the Data-Shield table definition and saved-set include from your persistent configuration. After confirming nothing else uses this reserved table, remove its live rules and sets with the command below. Other tables remain intact. Check the complete persistent configuration before any reload.

nftables / scoped rollback
sudo systemctl disable --now data-shield-nftables.timer
sudo systemctl stop data-shield-nftables.service
sudo nft delete table inet data_shield

Validation scope. Complete-feed loading, membership comparison, additions, removals and failed-batch rollback were tested in an isolated namespace. No production firewall was changed. Capacity, boot ordering, other firewall managers and real traffic enforcement require validation on your host.

BETTER DATA. TOGETHER.

Need a correction?

Report a documentation issue or request a review of an address. Keep credentials and private configuration out of public issues.

Report an issue ↗Block request ↗Whitelist request ↗