Verdict maps are how nftables lets you route packets based on what they are, not just where they’re going.

Let’s watch a packet get routed. Imagine a simple nftables rule:

table ip filter {
  set ip_source_addresses {
    type ipv4_addr
    flags interval
    elements = { 192.168.1.0/24, 10.0.0.5 }
  }

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

    ip saddr @ip_source_addresses meta mark set 0x1
    ip saddr @ip_source_addresses accept
    meta mark 0x1 jump mark_handled
  }

  chain mark_handled {
    # This chain is just a placeholder to show the jump
    # In a real scenario, you'd have actions here.
    counter
  }
}

When a packet from 192.168.1.10 hits the input chain, nftables checks if its source IP is in the ip_source_addresses set. It is. So, the packet gets a 0x1 mark. Then, the rule ip saddr @ip_source_addresses accept would normally accept it. But because the meta mark set happened before the accept, the packet continues to be evaluated. The next rule, meta mark 0x1 jump mark_handled, sees the 0x1 mark and jumps the packet to the mark_handled chain.

This is the core idea: nftables can look at the packet’s characteristics (source IP, destination port, protocol, even flags set by earlier rules) and then decide what to do with it, beyond just accept/drop/reject. Verdict maps are the mechanism that makes this decision-making process flexible and powerful.

The most surprising thing about verdict maps is that they aren’t a single, monolithic feature, but rather a combination of sets, maps, and the meta mark or meta skuid statements. You’re not "using a verdict map" directly; you’re building one by telling nftables to classify packets and then react to that classification.

Here’s how you build a more sophisticated routing decision:

table ip mangle {
  set tcp_ports_to_prioritize {
    type integer
    elements = { 80, 443, 22 }
  }

  map port_to_class {
    type { integer : integer }
    flags interval
    elements = {
      80 : 1,
      443 : 1,
      22 : 2
    }
  }

  chain prerouting {
    type filter hook prerouting priority -150; policy accept;

    # If it's TCP and the destination port is in our set,
    # use the map to assign a class (mark)
    tcp dport @tcp_ports_to_prioritize meta mark set @port_to_class

    # Now, based on the mark, we can jump to different chains
    meta mark 1 jump prioritize_http_https
    meta mark 2 jump prioritize_ssh
  }

  chain prioritize_http_https {
    # Packets from port 80 or 443 (marked with 1) land here
    counter name "http_https_traffic"
    # In a real setup, you might add them to a queue or a specific routing table
    accept
  }

  chain prioritize_ssh {
    # Packets from port 22 (marked with 2) land here
    counter name "ssh_traffic"
    # Different treatment for SSH
    accept
  }
}

In this example, we have a set of TCP ports we care about (tcp_ports_to_prioritize). We then use a map (port_to_class) to assign a numerical "class" to these ports: ports 80 and 443 get class 1, and port 22 gets class 2. The crucial line is tcp dport @tcp_ports_to_prioritize meta mark set @port_to_class. This tells nftables: "If the packet is TCP and its destination port is in tcp_ports_to_prioritize, look up that destination port in the port_to_class map, and whatever value you get (1 or 2), set that as the packet’s mark."

After the packet is marked, subsequent rules in the prerouting chain can inspect meta mark. meta mark 1 jump prioritize_http_https sends packets marked 1 to the prioritize_http_https chain, and meta mark 2 jump prioritize_ssh sends packets marked 2 to prioritize_ssh. This effectively routes packets based on their classification by the map.

The power here is that the classification logic (the map) is separate from the routing decision (the jump). You can change the map to assign different classes to ports, or add new ports to the set and map, without rewriting the core routing logic.

What most people miss is how meta mark can be used not just for simple integer values, but as a lookup target for maps themselves. The syntax meta mark set @map_name is what enables this dynamic classification. It’s not just setting a static value; it’s saying "set the mark to whatever this map tells you to set it to, based on the packet’s current attributes."

The next step is often to use these classified packets for more advanced queuing or policy-based routing, which might involve interacting with traffic control (tc) or routing tables.

Want structured learning?

Take the full Nftables course →