nftables’ output chain is where the system decides if your machine itself can send packets out to the network. It’s the final gatekeeper for your own outgoing traffic, deciding if your DNS queries, SSH connections, or even just pings get to leave your machine.
Let’s see it in action. Imagine you want to allow SSH (port 22) from your machine but block everything else.
table ip filter {
chain output {
type filter hook output priority 0; policy accept;
# Allow established and related connections
ct state established,related accept
# Allow SSH
tcp dport 22 accept
# Drop everything else
drop
}
}
If you apply this, you can SSH out, but your browser won’t load pages, and your ping will fail.
# This will work
ssh user@remote-server
# This will hang or fail
curl google.com
# This will fail
ping google.com
The output chain, like other nftables chains, is a sequence of rules. When a packet originating from your machine hits the output hook, nftables walks through the rules in that chain. The first rule that matches the packet’s characteristics (source/destination IP, port, protocol, connection state, etc.) determines the packet’s fate: accept, drop, or reject. If no rule matches, the packet’s fate is decided by the chain’s policy (which we’ve set to accept here, meaning by default everything is allowed unless explicitly dropped).
The type filter hook output priority 0; part is crucial. type filter tells nftables this chain is for packet filtering. hook output attaches it to the output path, meaning it inspects packets generated by the local system. priority 0 means it’s evaluated at the standard filtering priority; lower numbers run earlier. The policy accept sets the default action if no rule matches.
The ct state established,related accept rule is a common and important one. ct refers to connection tracking. This rule says if a packet is part of an already established connection (like a response to a request you made) or is related to an established connection (like an FTP data connection), it should be allowed. This is vital for any interactive or stateful communication. Without it, your replies to incoming packets would be dropped by the output chain.
The tcp dport 22 accept rule is specific. It says if the packet is TCP and its destination port is 22 (SSH), accept it. This allows your machine to initiate SSH connections.
Finally, drop is a terminal rule. If a packet reaches this point, it means it didn’t match any of the preceding accept rules. This rule silently discards the packet. There’s no notification sent back to the sender. If you wanted to explicitly deny and send a notification back, you’d use reject instead of drop.
The output chain is your control panel for what your system can initiate. You can, for example, block all outbound traffic except for DNS queries to a specific server.
table ip filter {
chain output {
type filter hook output priority 0; policy drop; # Default to drop
# Allow established and related connections
ct state established,related accept
# Allow DNS to specific server
udp dport 53 ip daddr 8.8.8.8 accept
tcp dport 53 ip daddr 8.8.8.8 accept
}
}
Here, we flipped the policy to drop. Now, nothing can leave unless explicitly allowed. We then allow established/related traffic and DNS (UDP and TCP on port 53) only to Google’s DNS server (8.8.8.8). Any other outbound attempt will be silently dropped.
The priority value in the hook definition is more nuanced than just "order." Different kernel modules can register hooks at different priorities. Standard filtering hooks are typically around priority 0. Network Address Translation (NAT) hooks, for instance, have different priorities. If you have NAT rules that need to run before filtering (e.g., to change the source IP before the output chain sees it), you might need to adjust priorities accordingly. However, for basic filtering, priority 0 is standard.
The next thing you’ll likely wrestle with is how to manage the output chain rules for different network interfaces, perhaps allowing full outbound access on one interface but restricting it on another.