Two virtual machines on the same host reach the LAN through a bridge, and a rule placed in the ip forward chain shows a counter that never moves. The frames are real, the traffic is passing, and the rule is simply not on its path. Comparing the two ways Linux exposes bridged traffic explains the silence, and it also explains why loading one legacy module is a worse answer than it looks.

Two paths through one host

A packet can leave this host in two very different ways, and the difference decides which hooks see it. If the destination is beyond a physical port and the frame is switched between two bridge ports, the host forwards an Ethernet frame; the routing code is not consulted, and no routed hop occurs. If the destination is the host itself, or a network reached through a tunnel interface, the packet is routed and the IP-layer hooks do apply. Keep that fork in mind, because every later question about chains and families follows from which of the two a frame takes.

That distinction is the whole story behind a rule that counts nothing. A bridged frame from veth9a1 to enp3s0 crosses the bridge, not the IP stack, so an ip forward chain sees nothing to match. Interface names in an older example do not settle which path you have, which is why the first step is to look at the topology rather than at the ruleset.

A tunnel adds a third case worth naming. A TUN device delivers IP packets to the host, so those are routed and do traverse IP chains, while a TAP device attached to a bridge delivers frames and behaves like the bridged case. The device type, not the product name, decides.

Compare the two families that can see a bridged frame

Linux supports native bridge filtering through nftables' bridge family, and it does not need any legacy module to inspect and drop frames in flight. The kernel's own bridge documentation makes that recommendation directly, which is worth knowing before a search result tells you to load a module.

Observation pointWhat it seesWhat it costs
nftables bridge family chainsEthernet frames forwarded between bridge ports, in both directionsRules must be written in bridge syntax with link-layer matches
Legacy br_netfilter plus an ip chainBridged IPv4 and IPv6 packets, re-exposed through IP-layer hooksExisting host rules may begin matching traffic they never saw before
inet chain for routed trafficPackets the host routes, including traffic arriving from a TUN deviceNo visibility into purely switched frames

Using the nft command instead of iptables does not, by itself, place a rule in the right family. A chain declared in the bridge family sees frames; a chain declared in ip, ip6 or inet does not, no matter which binary created it. The family is a property of the chain, not of the tool that typed it.

The legacy path works differently and that is the reason to compare rather than to pick a side by habit. Loading br_netfilter and enabling the bridge-nf-call settings re-exposes bridged IPv4 and IPv6 traffic through the IP-layer hooks, so an ordinary iptables rule can finally match it. The frames do not change; the set of hooks they traverse does, which means every existing host rule now has a new audience.

Read the effective path before adding a rule

Three commands establish where you are: what the bridge is attached to, whether the legacy module is active, and what chains already exist in the bridge family. Run them before writing anything, because the answer decides which of the two designs you are extending. Skipping this step is how a correct rule ends up in a chain that nothing traverses.

bridge link show
lsmod | grep br_netfilter
nft list ruleset | grep -A4 "chain forward"

The output separates the cases cleanly. Ports listed under the bridge tell you which interfaces are switched; an empty lsmod result means no IP-layer visibility of bridged frames at all; an existing bridge-family forward chain means a policy owner already exists and your rule belongs there rather than in a new chain of its own.

Then add one rule that is easy to read back, with a counter and no verdict change, and send a single representative flow through the bridge. A counter that moves proves the frame reached the rule. That is all it proves, so follow it with the denied path as well, since a policy that accepts everything also produces a moving counter.

nft add rule bridge filter forward iifname "veth9a1" oifname "enp3s0" counter accept

Where the legacy module changes behavior

Enabling the legacy path on a running bridge is not a neutral act of adding visibility. Rules that were previously unreachable for bridged traffic become reachable, including host rules written for a different interface, and a default drop policy in the ip forward chain can suddenly apply to frames that used to pass untouched. On a host managed over that bridge, the result is a lockout with no console. That is not a hypothetical failure mode; it is the ordinary consequence of widening a hook set on a machine you reach through the path you just widened.

The safer sequence is to decide which family owns the policy, write the rules there, and keep an independent recovery path before changing any sysctl. If two families both hold rules, document which one is authoritative; two overlapping policies are how a packet gets accepted in one place and dropped in another with no single file to read.

Isolation itself still needs a complete design. Permitting one source to one destination proves only that one flow works, while the requirement is usually that a VM cannot reach its neighbours or the host. Counters on the deny rules matter more than counters on the accept rule, because the accept counter is the one you already expected to move.

Finally, keep the host-input case separate. Traffic addressed to the bridge's own IP arrives at input, not forward, and a rule in forward will never see it no matter which family you choose. Three cases, two families, one lesson: the path decides the hook, and the hook decides whether your rule exists at all.

Akụkọ ndị metụtara ya