Skip to content

Introduction to netfilter and nftables (2022-04-02)

This guide introduces nftables by building a basic ruleset that allows us to safely forward packets between internal and external networks while also masquerading traffic with the IP address of the external network interface.

Read the entire guide carefully before attempting to install the template or any of the other tools referenced here.

What is nftables

nftables is the default tool used to manage netfilter's packet filter rules in Raspberry Pi OS. Though nftables was originally released in 2014, it did not make its way into Raspberry Pi OS until the Bullseye release in November 2021. nftables is the successor to iptables, which has been in use within the Linux community for more than 20 years.

nftables is a packet filtering framework included in a variety of modern Linux implementations. Packet filtering is a powerful technique that enables administrators to analyze and control the flow of inbound and outbound network traffic from any endpoint or intermediate network node. This level of control is essential for host- and network-based firewalls, network address translation (NAT), and other applications.

Packet filters define rules to match traffic based primarily on Layer 2 - 4 properties, such as: the inbound or outbound interface, type and length of messages, addresses, ports, and connection state. Each filter also describes actions taken when a match is found, e.g., log, deliver, or drop the packet. netfilter-based packet filters can also be used to apply NAT, make decisions about Quality of Service, or to reroute packets mid-flight.

nftables builds on netfilter, an open-source project adding hooks, connection tracking, and other capabilities to the Linux network stack. These components enable dynamic filters, like nftables rules, to be applied to every packet traversing the network stack. They are the secret sauce to many of the applications described above.

Tables, Chains, and Rule Syntax

In nftables, rules are composed of expressions that are used to select packets and statements that determine what actions netfilter will perform when a match occurs. While rules are the meat of our filter configurations, nftables doesn't allow them to stand alone. And so, before we explore the rule syntax, we need to looks tables and chains.

Tables

nftables uses a basic structure known as a table to organize rules. Each table is associated with a single address family and includes rules to apply to packets of that family.

This guide is focused on layer-3 traffic and considers tables for the following three address families:

  • ip: for IPv4 rules (default)
  • ip6: for IPv6 rules
  • inet: for rules that apply to both IPv4 and IPv6 packets

While its predecessor, iptables, included a default set of tables for common packet filter functionality, nftables administrators are left to define their own tables. For this exercise, we'll create an IPv4 nat table to implement address translation between private and public addresses and an IPv4/IPv6 filter table to protect the Pi and the rest of our LAN from malicious traffic.1

Example: Empty filter and nat tables

table inet filter {
    # Define chains (and rules) for IPv4 or IPv6 packets
}

table ip nat {
    # Define chains (and rules) for IPv4 packets
}

Chains

Within each table, rules are grouped in chains. A chain is an ordered list of rules describing the packets to match and the action to take when a match is found. When a chain is processed, the rules in it are tested in order from the beginning until a match is found. If nftables finds a match while evaluating a rule, the remaining rules in that chain may be skipped depending on what action is taken.

Base Chains

nftables won't evaluate the rules in a chain automatically. Rather, a chain will only be processed if it is explicitly called from a rule in another chain or if the chain has been associated with a netfilter hook.A chain that is associated with a hook is called a base chain.

Within the filter table, we are going to work with three base chains named input, output, and forward. These chains will be associated with filter hooks of the same names2, causing them to be processed when packets are being sent to software running on the Pi (input hook), when packets are being sent out by software on the Pi (output) hook, or when packets are being routed by the Pi from one network to another (forward hook).

Example: Filter table and empty base chains

table inet filter {
    chain input {
        type filter hook input priority 0;
        # Rules are evaluated when a packet is intended for this host
    }
    chain forward {
        type filter hook forward priority 0;
        # Rules are evaluated when an incoming packet will be forwarded by this host
    }
    chain output {
        type filter hook output priority 0;
        # Rules are evaluated when a packet is being sent from this host
    }
}

The nat table will have one base chain that is named for its respective hook, i.e., postrouting. Within netfilter, the postrouting hooks are the last ones to be processed when sending or forwarding a packet. As such, rules processed here can perform masquerading on the source IP addresses.3

Example: NAT with postrouting base chain

table ip nat {
    chain postrouting {
        type nat hook postrouting priority 100;
        # Rules are evaluated after forwarding decisions in order to support source NAT or masquerading
    }
}

Rules Syntax

The basic nftables rule syntax includes an expression and a statement. Expressions define conditions that need to be satisfied before an action is applied to a packet. Statements define the actions that will be taken. Since actions are the simpler component of a rule, we'll begin our exploration there.

Statements (actions)

nftables statements are used to control packet flow, apply NAT transformations, compute metrics, record logs, and modify the order of rule evaluation. While the list below is not exhaustive, it is sufficient for this guide and the beginning firewall developer.

  • accept: Stop processing chain and deliver packet
  • drop: Stop processing chain and drop packet
  • counter: Increment byte and packet counters and continue processing chain
  • log: Write information about packet to logs and continue processing chain
  • masquerade: Replace source IP address with the address of the output interface and stop processing chain

You may observe that actions like drop and accept cause nftables to stop processing the current chain. These actions are known as terminal actions. Actions like log and counter are non-terminal because they allow nftables to continue processing even if a match had been found. nftables supports multiple actions in a single statement, but a terminal may only appear as the final component of a rule.

For example, we can produce a rule that increments a counter and delivers a packet by ending our rule with counter accept. We can also write a rule that increments a counter and logs a packet without specifying a policy decision by ending the rule with counter log or log counter. However, we cannot combine terminals or include a terminal before a non-terminal as with accept counter or drop accept.

Expressions (conditions)

Our rules need to describe the packets we want to filter or manipulate. In this exercise, our primary consideration will be the direction in which a given packet is moving through the Pi. This is particularly of importance for routing decisions and NAT. We can describe our criteria by examining the ingress network for incoming packets and/or the egress network interface for outgoing packets.

Attention

Throughout the instructions, we'll refer to the internal interface as our <LAN> (because it serves our local network) and the external interface as the <WAN> (because it connects us to the Internet). Your configuration files will reflect the system assigned interface names, such as: eth0 and wlan0.

It's up to you to determine, based on your learning so far, which interface corresponds to which placeholder.

Let's look at a rule that we can use to apply address translation to outbound packets. Since outbound packets will leave on the WAN, we can specify this restriction using the outbound interface name (oifname) condition, i.e., oifname <WAN>. The following example masquerades outbound traffic on the <WAN> interface.

Example: Applying NAT transformation to outbound traffic

table ip nat {
    chain postrouting {
        type nat hook postrouting priority srcnat;
        oifname <WAN> masquerade
    }
}

We can be even more specific with our rules by writing an expression with multiple conditionals. To identify outbound traffic forwarded from the LAN to the WAN, we can check both the inbound and outbound interface names by writing iifname <LAN> oifname <WAN>. The following example demonstrates a rule that explicitly accepts traffic flowing from <LAN> to <WAN>.

Example: Combining multiple expressions

table inet filter {
    chain forward {
        type filter hook forward priority 0;
        iifname <LAN> oifname <WAN> accept
    }
}
Using Connection State in Rules

Netfilter is a stateful filtering framework, meaning that it is even possible to specify nftables rules based on the state of a network connection for a given packet. This feature shapes our rules in two ways.

First, you'll notice that we only had to specify the masquerade rule in the egress direction. The same rule also handles the de-masquerading process for ingress packets.

Second, you'll see that we can base filtering decisions on connection state itself. While it's appropriate for NAT routers to allow outgoing traffic, we typically want to place restrict incoming traffic. When configuring a gateway connection for a home or office, we often block incoming traffic unless it relates to existing connections from the LAN. Unsolicited traffic does not make sense in this context and poses a security risk.

To accomplish this feat, we will make use of netfilter's connection tracking extensions and restrict incoming traffic to packets that are part of an established connection or related to a recent egress packet. The connection tracking portion of this expression is ct state established,related.

Example: Using the connection tracking extension

    table inet filter {
        chain forward {
            type filter hook forward priority 0;
            iifname <WAN> ct state established,related accept
        }
    }

Default Policy (for Base Chains)

We are missing one piece to complete our configuration. By default, nftables accepts a packet unless a match occurs on a terminal rule. Base chains can override this behavior by defining a default policy. We'll use this feature to create a firewall that fail securely, i.e., blocking traffic that we don't explicitly allow.

The following example adds a base chain policy in order to drop forwarded packets that we didn't allow from another rule.

Example: Default drop policy

table inet filter {
    chain forward {
        type filter hook forward priority 0; policy drop;

        # Explicitly allow "safe" connections
        iifname <LAN> oifname <WAN> accept
        iifname <WAN> ct state established,related accept
    }
}

The default deny posture is a best practice for your networks and devices, but it's important to approach the strategy with care. Without additional rules, attaching policy drop to the input and output chains will block inbound and outbound SSH and other protocols that might be needed for remote management. We will come back to this in a later guide, but we recommend for now that you leave the default accept policy on these chains.

Final NAT Template

The following template draws together all of the components we have discussed so far to define a basic NAT configuration for a Linux router, leaving placeholder4,5 elements for the reader to complete. Please review the previous examples and specifications that have been given in order to create a valid configuration. The following section will help you test and install your new ruleset.

!!! info Routing / NAT template

# Always flush the active ruleset before defining your new rules
flush ruleset

table inet filter {
    chain forward {
        type filter hook <HOOK> priority 0; policy <ACTION>;

        # Explicitly allow "safe" connections
        iifname <LAN> oifname <WAN> <ACTION>
        iifname <WAN> ct state established,related <ACTION>
    }

    chain input {
        type filter hook input priority 0;
        # You may leave this chain empty for now
    }

    chain output {
        type filter hook output priority 0;
        # You may leave this chain empty for now
    }
}


table ip nat {
    chain postrouting {
        type nat hook postrouting priority srcnat;
        oifname <WAN> masquerade
    }
}

Configuring nftables

Throughout this guide, nftables configuration has been presented in a declarative, table-based format that closely reflects the table/chain/rule hierarchy. nftables also supports a command-based syntax that is passed to the nft utility on the command line or from within scripts in order to create, read, update, and delete individual elements of a ruleset dynamically.

We generally want firewall and routing rules to be configured as soon as possible during the boot process. When the nftables service is started, Raspberry Pi OS will initialize it with the contents of /etc/nftables.conf.

While experimentation is usually encouraged, you may encounter negative consequences if you make haphazard modifications to this file. A simple script is provided below to support the learning process by testing your ruleset before making changes permanent.

Enable the nftables Service

The nftables service may not be running by default on your device. You can check on the current status of the daemon with systemctl status nftables. To launch the service immediately, run systemctl start nftables. To ensure that nftables also starts automatically at boot, run systemctl enable nftables.

Editing nftable Rules Safely

Here be Dragons

Do not write rules directly to /etc/nftables.conf. Without proper testing, you risk creating a rule that locks you out of your device from the network.

Create a working copy of /etc/nftables.conf into your home directory (it is okay to rename the file). Open the new copy and ensure that flush ruleset near the top (before your table definitions).6 Proceed with any modifications to the working copy.

To test your ruleset, we recommend a scripted approach that can automatically revert a change that locks you out of the Pi. INFO314 students can find a copy of nftables-apply.sh in the project repository or create the script from the following listing.

nftables-apply.sh
#!/bin/bash

# Adapted from https://sanjuroe.dev/nft-safe-reload (retrieved on 2022-04-03)
# Modified to avoid editing system rules in-place

SYSTEM_RULES="/etc/nftables.conf"
TIMEOUT=45

saved_rules=$(mktemp)
cleanup() {
    rm -f "$saved_rules"
}

trap "cleanup" EXIT;

# Waits $TIMEOUT seconds for a yes/no response
read_approval() {
        read -t $TIMEOUT response 2> /dev/null

        case "$response" in
                y|Y)
                        return 0
                        ;;
                *)
                        return 1
                        ;;
        esac
}

# Make a copy of the active ruleset
backup() {
        printf "flush ruleset\n" > $saved_rules
        nft list ruleset >> $saved_rules
}

# Apply a named ruleset
apply() {
        nft -f "$1"
}

# Update system ruleset
save() {
    local source_rules="$1"
    cp --no-preserve=mode,ownership "$source_rules" "$SYSTEM_RULES"
}

# Make a backup of the active ruleset in case of rollback
backup
new_rules="$1"

if apply "$new_rules"; then
    printf "Are you still able to connect to your device (auto-rollback in $TIMEOUT seconds)? [y/n] "
        if read_approval; then
        save "$new_rules"
                printf "\nUpdated system ruleset at ${SYSTEM_RULES}\n"
        exit 0
        else
                apply "$saved_rules"
        printf "\nRolling back to original configuration\n"
                exit 2
        fi
fi

To test your rules, pass the name of your new rules file to the nftables-apply.sh script, e.g., ./nftables-apply.sh nftables-test.conf. If your rules load without any errors, you will be prompted to answer whether you can still connect.

Open an additional terminal window and launch SSH to connect to your Pi. If you are able to complete this step successfully, you can apply your changes to /etc/nftables.conf by answering y at the nftables-apply prompt in your original session.

Accept New Rules

If the test fails, answer n or wait 45 seconds for the rules to revert automatically.

Reject New Rules

Once you are satisified that your rules are working correctly, it's a good idea to verify that your rules are loaded and that everything works correctly after a reboot. Use the nft list ruleset command to view active rules.

Resources


  1. These names were chosen to mirror the built-in tables of iptables

  2. This naming convention is influenced by iptables and its built-in chains. 

  3. netfilter also defines a prerouting nat hook that executes before routing decisions and can be used to implement port-forwarding and other destination NAT configurations. 

  4. Text surrounded by <> symbols is indicative of a placeholder that should be filled in before loading the ruleset. 

  5. Do not quote text substituted for placeholders unless quotes are shown in the template. 

  6. nft -f <FILENAME> is additive by default. Including flush ruleset before your table/chain definitions ensures that the new ruleset is properly loaded. 

Back to top