Bridging traffic at L2 with nftables is surprisingly powerful because it lets you apply firewall rules to packets before they’re even handed off to an IP stack, meaning you can block things like MAC spoofing or rogue DHCP servers at the network edge.
Let’s see it in action. Imagine you have two network interfaces, eth0 and eth1, that you want to bridge together into a single network segment. You’re running a DHCP server on this segment and want to ensure only your legitimate server hands out IP addresses.
# Create a bridge interface
sudo ip link add name br0 type bridge
# Add physical interfaces to the bridge
sudo ip link set eth0 master br0
sudo ip link set eth1 master br0
# Bring up the interfaces
sudo ip link set eth0 up
sudo ip link set eth1 up
sudo ip link set br0 up
# Now, let's add nftables rules to filter traffic on the bridge
sudo nft add table bridge filter
sudo nft add chain bridge filter ingress { type filter hook ingress priority -400 \; }
sudo nft add chain bridge filter forward { type filter hook forward priority 400 \; }
sudo nft add chain bridge filter output { type filter hook output priority 400 \; }
# Block all DHCP packets except from your legitimate server (MAC: 00:11:22:33:44:55)
sudo nft add rule bridge filter ingress ip protocol udp th dport 67 ct state new accept
sudo nft add rule bridge filter ingress ip protocol udp th dport 67 mac saddr != 00:11:22:33:44:55 drop
# Allow established and related DHCP traffic (replies from the server)
sudo nft add rule bridge filter ingress ip protocol udp th sport 67 ct state established,related accept
# Block ARP requests from unknown MAC addresses (example: block MAC spoofing)
sudo nft add rule bridge filter ingress arp ether saddr != AA:BB:CC:DD:EE:FF accept
sudo nft add rule bridge filter ingress arp ether saddr != 00:11:22:33:44:55 drop # Example: allow specific MACs to broadcast
The ingress hook is where the magic happens for L2 filtering on a bridge. Packets hit the ingress hook on the bridge interface as they arrive, before being delivered to any specific port (or processed by the IP stack if they’re destined for the host). This means you can inspect and filter based on L2 information like MAC addresses. The forward hook is for traffic passing through the bridge between interfaces, and output is for traffic originating from the host itself.
The mental model here is that nftables can operate at different "hooks" within the network stack. For bridges, ingress is your primary tool for L2 filtering. You’re essentially saying "as a packet enters the bridge device, check its MAC address and UDP destination port." If it’s a new DHCP request (ct state new) and it’s going to port 67 (DHCP server port), we first accept it if it’s from our known server. Then, we drop any other new DHCP requests that don’t match that source MAC. We also accept established/related DHCP traffic to allow replies. For ARP, we accept requests from known MACs and drop others.
The ct state new part is crucial. It ensures we’re only applying the strict MAC check to new DHCP requests, not to ongoing conversations. Without it, you might block legitimate replies. Similarly, using arp in the rule targets ARP packets specifically, which is common for L2 security.
A common pitfall is forgetting to allow established/related traffic for protocols that use multiple connections or dynamic ports. For DHCP, while the initial request/offer might be on UDP 67, subsequent communication could potentially use different ports or rely on the connection tracking state. Always consider the full lifecycle of a protocol when applying stateful rules.
The next step is usually to integrate these L2 rules with L3/L4 filtering to create a comprehensive firewall policy.