5

Setup:

Ubuntu 20.04 on a box with 5 physical ethernet ports (let's call them eth1 - eth5).

I have joined them all to a bridge ("br0") with brctl and they can all ping each other.

What I'm trying to do: allow eth2 - eth5 to all continue chatting freely with each other while limiting what can come in via eth1. (Ultimately eth1 will be the "outside connection" for specific inbound traffic while the other interfaces can talk freely.)

iptables -A FORWARD -j DROP

Expectation: everyone goes blind. Works great. The bridge is using the rules from iptables. Test successful, undo that.

iptables -F
iptables -A FORWARD -i eth1 -j DROP

Expectation: eth1 goes blind but everything else is fine. Reality: nothing at all happens. I double checked the iptables -L and the only thing in there is a single "DROP all -- anywhere anywhere" underneath Chain FORWARD.

iptables -F
iptables -A FORWARD -i br0 -j DROP

That seemed to have an impact. The connected laptops can still ping the bridge itself but they can't ping each other.

Why doesn't it work when I drop a specific interface?

I'm also open to suggestions such as "you are going about this entirely the wrong way". Note also: I am trying to avoid any sort of ip filtering. I want to filter by the interface (physical ethernet ports), not by ip address of what happens to be plugged into them.

1 Answer 1

12

iptables works at the routing layer. It doesn't work at the bridging layer. Once eth1 was set as bridge port it's not involved in routing anymore. That means there won't be a packet seen routed through eth1, and normally it won't traverse iptables at all.

br_netfilter, bridging and iptables

There's a catch: when the br_netfilter module is loaded (typically by Docker), iptables will also filter bridged IPv4 frames. OP's question hints the module is loaded. This case is shown in the schematic below with the green boxes (iptables handling IPv4) in the blue field (Ethernet bridging):

Packet flow in Netfilter and General Networking

In this case, any bridged frame will appear to iptables through the bridge that handled this frame (br0) instead. eth1 still cannot be referenced like a routed interface. Instead this requires the special physdev module (which also triggers the loading of br_netfilter) to specify a bridge port. One must take care to distinguish bridged traffic from routed traffic, because iptables will see both. This documentation can help with more complex rulesets: ebtables/iptables interaction on a Linux-based bridge section 7.

So in the end, when the module br_netfilter is loaded, or anyway gets loaded by the following rules, to achieve the goal, the very simple example below will do. As I don't have context from OP about Docker or anything else present, I must force FORWARD's policy to ACCEPT and insert rules first because if the default policy was DROP and Docker loaded its own rules, this simple example would be far from enough:

iptables -P FORWARD ACCEPT
iptables -F FORWARD

iptables -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -I FORWARD 2 -m physdev --physdev-in eth1 -j DROP

nftables

This requires kernel >= 5.3.

Note that if there's no reason to load br_netfilter (such as running Docker), instead of using iptables for this, one could use nftables directly in the proper place: the bridge family, because nftables can use the conntrack facility for a stateful firewall directly in the bridge family, without this kludge (initially because ebtables cannot use conntrack).

So instead one could remove the br_netfilter module altogether:

rmmod br_netfilter

Doing this will disrupt Docker.

Or just disable br_netfilter's feature if loaded (a failed test is supposed to mean the module is not loaded) in the current network namespace and on the current bridge (where it was not enabled by default anyway but just to be thorough):

if sysctl -n net.bridge.bridge-nf-call-iptables 2>/dev/null; then
    sysctl -qw net.bridge.bridge-nf-call-iptables=0
    ip link set dev br0 type bridge nf_call_iptables 0
fi

The per-namespace toggle or the bridge toggle is enough for the feature to be activated: both must be disabled to disable it.

Else it will still affect iptables and also nftables in the ip family (nftables has no tool to cope with this properly). Doing this will also disrupt Docker if it's running and in the same network namespace (the host), unless it's actually a Docker-in-Docker running in an other network namespace.

The equivalent nftables ruleset directly in the bridge family is (load with nft -f ...):

table bridge t {
    chain forward { type filter hook forward priority filter; policy accept;
        ct state established,related accept
        iif eth1 drop
    }
}

3
  • 1
    For bridge to pass packets through iptables a setting needs to be enabled on a bridge, see ip -d link show br0. Settings are: nf_call_iptables, nf_call_ip6tables, nf_call_arptables and can be either 0 (disabled) or 1 (enabled). Previously these settings were system-wide and set up with sysctl. Apr 7, 2023 at 3:22
  • @NikitaKipriyanov Good point. Setting is still enabled system-wide by default as soon as br_netfilter is loaded (including in newer kernels >= 5.3 where it becomes per namespace) and overrides the per-bridge setting, but yes, this would still help to just change the setting to lower disruption and especially avoid it being re-enabled next time the module is reloaded. Some details in this UL SE Q/A: unix.stackexchange.com/questions/719112/… . I'll set it to 0.
    – A.B
    Apr 7, 2023 at 6:50
  • nfttables is a rabbit hole and a half, but this got me pointed in the right direction, and seems a better solution than what I was doing with iptables. Thanks!
    – JamieB
    Apr 7, 2023 at 20:50

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .