nftables expressions are the core mechanism for defining packet matching rules, and they can do far more than just simple IP address checks.

Let’s see nftables in action with a few examples. Imagine we’re building a firewall for a web server.

# Flush existing rules and chains
nft flush ruleset

# Create a new table named 'inet filter'
nft add table inet filter

# Create chains for incoming, forward, and outgoing traffic
nft add chain inet filter input { type filter hook input priority 0 \; policy accept \; }
nft add chain inet filter forward { type filter hook forward priority 0 \; policy accept \; }
nft add chain inet filter output { type filter hook output priority 0 \; policy accept \; }

# Allow established and related connections
nft add rule inet filter input ct state established,related accept

# Allow SSH from a specific trusted IP
nft add rule inet filter input ip saddr 192.168.1.100 tcp dport 22 accept

# Allow HTTP and HTTPS traffic
nft add rule inet filter input tcp dport { 80, 443 } accept

# Drop all other incoming traffic to the web server
nft add rule inet filter input drop

In this setup, inet filter input is a chain, and ct state established,related accept is an expression. The ct state part refers to the connection tracking system, and established,related is a set of keywords that the expression matches against. accept is the verdict, the action taken when the expression matches.

The real power comes from combining these. nftables expressions are built using a rich set of operators that can inspect virtually every part of a network packet.

Here’s a breakdown of some common operators and how they’re used:

  • IP Layer:

    • ip saddr <address>: Matches source IP address.
    • ip daddr <address>: Matches destination IP address.
    • ip protocol <protocol>: Matches IP protocol (e.g., 6 for TCP, 17 for UDP).
    • ip tos <value>: Matches Type of Service field.
    • ip frag : Matches fragmented packets.
    • ip dscp <value>: Matches Differentiated Services Code Point.
    # Allow UDP traffic from a specific subnet to port 53 (DNS)
    nft add rule inet filter input ip saddr 192.168.1.0/24 ip protocol 17 udp dport 53 accept
    

    This rule uses two IP-layer expressions (ip saddr and ip protocol) and a transport-layer expression (udp dport) to precisely target DNS queries from your local network.

  • TCP/UDP Layer:

    • tcp dport <port>: Matches destination TCP port.
    • tcp sport <port>: Matches source TCP port.
    • tcp flags <flags>: Matches TCP flags (e.g., syn, ack, fin).
    • udp dport <port>: Matches destination UDP port.
    • udp sport <port>: Matches source UDP port.
    # Block outgoing TCP traffic with the SYN flag set and ACK flag clear, unless it's to port 22 (SSH)
    nft add rule inet filter output tcp flags syn tcp !flags ack tcp dport != 22 drop
    

    This is a common technique to prevent certain types of network scans or unauthorized outbound connections. The ! negates the flags ack match.

  • ICMP Layer:

    • icmp type <type>: Matches ICMP message type.
    • icmp code <code: Matches ICMP message code.
    # Allow ICMP echo requests (ping)
    nft add rule inet filter input icmp type echo-request accept
    
  • Connection Tracking (ct):

    • ct state <state>: Matches connection state (e.g., established, related, new, invalid).
    • ct direction <direction>: Matches connection direction (ingress or egress).
    • ct count <number>: Matches the number of packets in a connection.
    # Drop packets that are part of an invalid connection
    nft add rule inet filter input ct state invalid drop
    

    This is a fundamental security rule. invalid state packets are those that don’t seem to belong to any legitimate connection.

  • Ethernet Layer:

    • ether saddr <mac>: Matches source MAC address.
    • ether daddr <mac>: Matches destination MAC address.
    • ether type <type>: Matches EtherType (e.g., 0x0800 for IPv4).
    # Allow traffic only from a specific MAC address on the local network
    nft add rule inet filter input ether saddr 00:11:22:33:44:55 accept
    
  • Bitwise Operations and Sets:

    • & (bitwise AND): Used for matching specific bits.
    • | (bitwise OR): Used for matching any of a set of bits.
    • { val1, val2, ... }: Defines a set of values for a match.
    # Allow traffic to ports 80, 443, or 8080
    nft add rule inet filter input tcp dport { 80, 443, 8080 } accept
    

    Using sets makes rules much more concise than writing individual rules for each port.

    # Example using bitwise AND to match specific TCP flags (e.g., only SYN and ACK set)
    # This is a more advanced example. For TCP flags, it's often easier to use the named flags.
    # For instance, to match SYN and ACK, you'd typically write: tcp flags syn,ack
    # This example demonstrates the bitwise operator for other scenarios.
    # Let's say we want to match packets where the 3rd and 5th bits of the TOS field are set.
    # (Assuming TOS bits map to values, e.g., 0x10 and 0x40)
    nft add rule inet filter input ip tos 0x50 meta (l4proto . tos) & 0x50 == 0x50 accept
    

    The meta expression is a powerful way to access metadata about the packet, including protocol and layer 4 information.

  • Regular Expressions (re):

    • payload <protocol> <offset>:<length> re <regex>: Matches a regular expression against a specific part of the packet payload.
    # Block HTTP requests containing the string "badstuff" in the URI
    nft add rule inet filter input tcp dport 80 payload http uri re "badstuff" drop
    

    This is a very powerful, though potentially resource-intensive, way to inspect packet contents. The payload http uri part tells nftables to look specifically at the URI part of an HTTP request.

  • Regular Expressions (er):

    • payload <protocol> <offset>:<length> er <regex>: Similar to re, but for extended regular expressions.

The most surprising true thing about nftables expressions is that they are evaluated lazily and short-circuit: as soon as an expression within a rule evaluates to false, the rest of the rule is not even checked, and the packet moves on to the next rule. This means the order of your expressions within a single rule matters for performance, even though the logical outcome is the same.

The full mental model of nftables rules is that each rule is a sequence of expressions that must all evaluate to true for the rule to match. If all expressions in a rule match, the rule’s verdict (e.g., accept, drop, reject, queue) is applied. If any expression fails to match, the next rule is checked. The meta expression is particularly useful for accessing packet information that isn’t directly part of the packet headers, like the ingress interface or the hook point.

One thing most people don’t know is how nftables handles sets internally. When you define a set, nftables can optimize lookups significantly, especially for large sets. It uses efficient data structures like hash tables or tries, making matches against hundreds or thousands of IPs or ports incredibly fast, often comparable to single-value lookups. This is why using sets like { 80, 443, 8080 } is so much more efficient than writing three separate rules with individual port matches.

The next concept you’ll likely encounter is nftables’ advanced features like logging, rate limiting, and NAT, which also leverage these powerful expression capabilities.

Want structured learning?

Take the full Nftables course →