If you find some discrepancy or missing information - open a GitHub issue

Was this information useful? Then star the repository on GitHub


Writing of this documentation is still in progress! Read it with a grain of salt!

Firewall - NFTables


Chain hooks/Table families


Packet flow




Kernel Modules

Some functionality of NFTables might not be enabled by default.

To check which was enabled at compile-time - check the config file:

cat "/boot/config-$(uname -r)" | grep -E "CONFIG_NFT|CONFIG_NF_TABLES"

To find all existing modules:

find /lib/modules/$(uname -r) -type f -name '*.ko' | grep -E 'nf_|nft_'

To enable a module:

modprobe nft_nat
modprobe nft_tproxy


Config File

NFTables can be completely configured from one or more config files.

Most times you might want to use:

  • a main config file: /etc/nftables.conf

  • a configuration directory to include further files: /etc/nft.conf.d/

The systemd service will load the main config file by default:

# /lib/systemd/system/nftables.service

ExecStart=/usr/sbin/nft -f /etc/nftables.conf
ExecReload=/usr/sbin/nft -f /etc/nftables.conf
ExecStop=/usr/sbin/nft flush ruleset

Main config file example:

#!/usr/sbin/nft -f
flush ruleset
include "/etc/nft.conf.d/*.conf"

Then you can add your actual configuration in the configuration directory!

To test your configuration:

nft -cf /etc/nftables.conf



THere are some libraries/modules that enable you to manage NFTables from code directly:


See: NFTables Ansible-Role



You can trace traffic that flows through you chains.

See also: NFTables documentation - trace

You need to:

  • Tag traffic you want to trace by adding the meta nftrace set 1 option to a rule.

  • Listen to this traces by running nft monitor trace in a separate terminal.

You may want to start the trace at the point where the traffic enters.

Example for input traffic:

chain input {
    type filter hook input priority 0; policy drop;

    # enable tracing for: tcp-traffic to port 1337 originating from a specific network
    tcp dport 1337 ip saddr meta nftrace set 1



Example for output traffic:

chain output {
    type filter hook output priority 0; policy drop;

    # enable tracing for: http+s to a specific target
    tcp dport { 80, 443 } ip daddr meta nftrace set 1



Example monitor information:

nft monitor trace
> trace id a95ea7ef ip filter trace_chain packet: iif "enp0s25" ether saddr 00:0d:b9:4a:49:3d ether daddr 3c:97:0e:39:aa:20 ip saddr ip daddr ip dscp cs0 ip ecn not-ect ip ttl 115 ip id 0 ip length 84 icmp type echo-reply icmp code net-unreachable icmp id 9253 icmp sequence 1 @th,64,96 24106705117628271805883024640
> trace id a95ea7ef ip filter trace_chain rule meta nftrace set 1 (verdict continue)
> trace id a95ea7ef ip filter trace_chain verdict continue
> trace id a95ea7ef ip filter trace_chain policy accept
> trace id a95ea7ef ip filter input packet: iif "enp0s25" ether saddr 00:0d:b9:4a:49:3d ether daddr 3c:97:0e:39:aa:20 ip saddr ip daddr ip dscp cs0 ip ecn not-ect ip ttl 115 ip id 0 ip length 84 icmp type echo-reply icmp code net-unreachable icmp id 9253 icmp sequence 1 @th,64,96 24106705117628271805883024640
> trace id a95ea7ef ip filter input rule ct state established,related counter packets 168 bytes 53513 accept (verdict accept)

Translate IPTables

Most times the behaviour of IPTables and NFTables is pretty much the same.

In some Distributions the default IPTables backend is already migrated to NFTables.

Why translate from IPTables?

There are 1000x more resources related to IPTables out there that might help you get things working.

I would recommend:

  • having a blank VM to test IPTables ruleset

  • save the working minimal-ruleset iptables-save > /etc/iptables/rules.ipt

  • translate the ruleset to nftables iptables-restore-translate -f /etc/iptables/rules.ipt > /etc/iptables/rules.nft

  • test the NFTables ruleset and remove the default chains you don’t need (IPTables is a little more messy with its defaults)

BTW: one can also restore IPTables rules by using iptables-restore < /etc/iptables/rules.ipt


NFTables base-config example


Quote from the tproxy kernel docs:

Transparent proxying often involves "intercepting" traffic on a router.
This is usually done with the iptables REDIRECT target; however, there are serious limitations of that method.
One of the major issues is that it actually modifies the packets to change the destination address -- which might not be acceptable in certain situations. (Think of proxying UDP for example: you won't be able to find out the original destination address. Even in case of TCP getting the original destination address is racy.)
The 'TPROXY' target provides similar functionality without relying on NAT.

This functionality allows us to send traffic to an userspace process and read/modify it.

This can enable powerful solutions! Per example see: blog.cloudflare.com - Abusing Linux’s firewall


TPROXY seems to only support local targets.

As one can see in the kernel sources - there is a check if the target port is in use: nft_tproxy.c



One thing you’ll need to know: The TPROXY operation can only be used in the prerouting - filter (mangle) chain!

Traffic that passes this chain/hook by default can easily be proxied.


Because of this - traffic that enters at the ‘output’ (originating from the same host) chain/hook can not be redirected directly.

We need to route it to ‘loopback’ so it passes through ‘prerouting’.

NOTE: This image shows the problem we are facing in a very abstract way. It might not display the traffic-flow in a correct manner!



You might want to target a remote proxy server. This does not work with this operation on its own.

One would need to use a proxy-forwarder tool that can handle this for you.

I’ve patched an existing tool for exactly this purpose: proxy-forwarder

With a tool like that you can wrap the plain traffic received from TPROXY and forward or tunnel it.

# NFTables =TCP=> TPROXY (forwarder @ =HTTP[TCP]=> PROXY

> curl https://superstes.eu
# proxy-forwarder
2023-08-29 20:49:10 | INFO | handler | <=> superstes.eu:443/tcp | connection established
# proxy (squid)
NONE_NONE/200 0 CONNECT superstes.eu:443 - HIER_NONE/- -
TCP_TUNNEL/200 6178 CONNECT superstes.eu:443 - HIER_DIRECT/superstes.eu -

> curl http://superstes.eu
# proxy-forwarder
2023-08-29 20:49:07 | INFO | handler | <=> superstes.eu:80/tcp | connection established
# proxy (squid)
TCP_REFRESH_MODIFIED/301 477 GET http://superstes.eu/ - HIER_DIRECT/superstes.eu text/html



To keep invalid configuration from stopping/failing your nftables.service - you can add a config-validation in it:

# /etc/systemd/system/nftables.service.d/override.conf

ExecStartPre=/usr/sbin/nft -cf /etc/nftables.conf

ExecReload=/usr/sbin/nft -cf /etc/nftables.conf
ExecReload=/usr/sbin/nft -f /etc/nftables.conf


This will catch and log config-errors before doing a reload/restart.

When doing a system-reboot it will still fail if your config is bad.


NFTables lacks some functionality, that is commonly used in firewalling.

You can add a scheduled scripts that add these functionalities to NFTables!

See: Ansible-managed addons


It is nice to have variables that hold the IPs of some DNS-record.

NFTables CAN resolve DNS-records - but will throw an error if the record resolves to more than one IP.. (Error: Hostname resolves to multiple addresses)

See: NFTables Addon DNS


This addon was inspired by the same functionality provided on OPNSense

It will download existing IPLists and add them as NFTables variables.

IPList examples:

See: NFTables Addon IPList


See: NFTables Addon Failover



See: Ansible-based examples

IPv4 Baseline

IPv6 Baseline

Security Baseline

Docker host

Proxmox host (PVE)

Forwarder (Router, Network firewall, VPN Server)