nftables named sets are a fundamental concept for managing groups of IP addresses or ports efficiently. They allow you to define a collection of elements once and then reference that collection multiple times within your firewall rules, making your rulesets cleaner, more readable, and significantly easier to maintain. Instead of repeating the same list of IPs or ports in multiple ip saddr { 1.1.1.1, 2.2.2.2 } or tcp dport { 80, 443 } statements, you can define a set like set ip { 1.1.1.1, 2.2.2.2 } and then use ip saddr @set_name or tcp dport @set_name.
Let’s see this in action. Imagine you have a web server and want to allow access from a few specific IP addresses. Without named sets, your rules might look like this:
table ip filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow SSH from specific IPs
tcp dport 22 ip saddr { 192.168.1.10, 192.168.1.11 } accept
# Allow HTTP from specific IPs
tcp dport 80 ip saddr { 192.168.1.10, 192.168.1.11 } accept
# Allow HTTPS from specific IPs
tcp dport 443 ip saddr { 192.168.1.10, 192.168.1.11 } accept
}
}
This quickly becomes repetitive if you have many services or a larger list of allowed IPs.
Now, let’s introduce a named set:
table ip filter {
set trusted_ips {
type ipv4_addr
elements = { 192.168.1.10, 192.168.1.11 }
}
chain input {
type filter hook input priority 0; policy drop;
# Allow SSH from trusted IPs
tcp dport 22 ip saddr @trusted_ips accept
# Allow HTTP from trusted IPs
tcp dport 80 ip saddr @trusted_ips accept
# Allow HTTPS from trusted IPs
tcp dport 443 ip saddr @trusted_ips accept
}
}
This is much cleaner. The trusted_ips set is defined once, specifying its type (ipv4_addr for IPv4 addresses) and its elements. Then, in the input chain, we simply reference this set using the @ symbol before the set name.
The mental model for named sets is straightforward: they are dictionaries or lookup tables managed by the kernel. When nftables evaluates a rule that uses a set, it performs a lookup against the set’s data structure. This lookup is extremely fast, especially compared to checking against a long, inline list of elements.
The primary benefit is maintainability. If you need to add or remove an IP from the trusted list, you modify the set definition in one place, and all rules referencing that set are automatically updated. This dramatically reduces the chance of errors when making changes.
Sets can also hold ports, protocols, MAC addresses, and even other sets. For example, you might have a set of trusted web ports and another set of trusted IPs, and then combine them:
table ip filter {
set trusted_ips {
type ipv4_addr
elements = { 192.168.1.10, 192.168.1.11 }
}
set web_ports {
type port
elements = { 80, 443 }
}
chain input {
type filter hook input priority 0; policy drop;
# Allow web traffic from trusted IPs to web ports
tcp dport @web_ports ip saddr @trusted_ips accept
}
}
You can also define sets with specific timeouts, which is particularly useful for dynamic sets that are populated by other rules (e.g., allowing established connections). The timeout parameter specifies how long an element remains in the set after its last use.
table ip filter {
set dynamic_ssh_allow {
type ipv4_addr
flags timeout
timeout 300 # seconds
}
chain input {
type filter hook input priority 0; policy drop;
# Allow established SSH connections
tcp dport 22 ct state established,related accept
# Allow new SSH connections from IPs that have recently connected
tcp dport 22 ct state new ip saddr @dynamic_ssh_allow accept
# Add source IPs of new SSH connections to the dynamic set
tcp dport 22 ct state new add @dynamic_ssh_allow { ip saddr ct_mark 0x1 }
}
}
In this example, dynamic_ssh_allow will hold IPs that have recently initiated an SSH connection. If an IP connects and then disconnects, it will remain in the set for 300 seconds, allowing it to reconnect within that window without needing a new explicit rule. The flags timeout is crucial here.
The most surprising thing about nftables sets is how they handle element types and can be implicitly converted or combined. While you define a set with a specific type (like ipv4_addr or port), when you use it in a rule, nftables infers the context. For example, ip saddr @my_set works because my_set is defined as ipv4_addr. If my_set contained mac_addr, that rule would fail. Similarly, if you define a set with type ipv4_addr but try to add a MAC address to it, it will error. This strict typing prevents subtle misconfigurations.
The true power of sets lies in their efficiency. The kernel’s set implementation uses highly optimized data structures (often hash tables) for rapid lookups. This means that even with thousands of elements in a set, checking for membership is nearly instantaneous. This performance advantage scales much better than long, comma-separated lists of addresses or ports within the rules themselves, which the kernel has to parse and evaluate for every packet.
The next concept you’ll likely encounter is how to manage these sets dynamically, especially in conjunction with connection tracking (ct) and other nftables features to create more sophisticated, stateful firewall policies.