Skip to main content

Poor man's Linux bridge port security

Bridge is a L2 device that brings two Ethernet segments together. A segment is a collision domain. Since we all use switches now, collision domains are restricted to single ports, so a segment in our modern world is just a path between your computer and the switch. In case there are more than 2 segments, such bridge is called a “switch”.

In 7-layer OSI model L2 is a Data Link layer and it sits between Physical layer (L1) which actually carries the bits back and forth and the Network layer (L3), which is what routers are busy working with. Bridge is what you usually have virtual machines connected to and this is what allows your wireless-enabled router to connect the wireless and wired interface together and have a single network configuration.

If you need to restrict access to a certain port on a standard Linux bridge (not an OpenvSwitch one), then you will have to add the rules to PREROUTING chain on nat table in ebtables.

You may want to group rules by port and not restrict the access to the bridge from all other ports the way libvirt is doing that:

ebtables -t nat -N port-eth0-1
ebtables -t nat -A port-eth0-1 -s  52:54:00:3f:e0:8c -j RETURN
ebtables -t nat -A port-eth0-1 -j DROP
ebtables -t nat -A PREROUTING -i eth0.1 -j port-eth0-1

Why is that even needed?

I have a br-lan bridge that has eth0.1 (LAN) and wlan0 (Wi-Fi) interfaces connected and I found that I can easily reboot my TP-Link 1043ND OpenWRT-based router with macof tool from dsniff package because the Linux bridge module does not limit the amount of MAC addresses it learns. It took me quite a while to find why this happens until I figured out that OpenWRT's busybox shell brctl applet lacks a vital command, brctl showmacs $bridge-interface. And when I installed a proper brctl from the repository and ran brctl showmacs br-lan after a second of macof, I was impressed.

root@gw:~# brctl showmacs br-lan | wc -l

By default the MAC addresses are set to expire after 5 minutes (300 seconds). After 5 minutes any hosts that haven't contacted the bridge during this time will be purged from the MAC address cache. macof can generate tens of thousands of Ethernet frames per second consuming the router's RAM even before the aging time lets any entries expire.

You can not set the number of MAC addresses that are allowed on the port as in IOS switchport port-security maximum value though, so it is not as versatile as dedicated managed switches can be, however it is enough to make sure that an untrusted machine connected to a linux-based router won't be able to crash it just by sending a ton of spoofed Ethernet frames.

Contrary to the documentation, brctl setageing $bridge_interface 0 does not make the existing entries permanent. It drops all the learned non-local addresses and stops learning the new ones effectively transforming the switch into a hub that floods all the interfaces when it receives a frame on one of its ports.

In case of libvirt machines, this can be accomplished by adding a no-mac-spoofing filter to the guest domain definition under network interface:

<interface type='network'>
    <mac address='52:54:00:82:e4:6c'/>
    <source network='vm100'/>
    <model type='virtio'/>
    <filterref filter='no-mac-spoofing'/>
    <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>

I found that ebtables was removed from OpenWRT at some point due to performance issues. My recent measurements, however, displayed almost no difference at all – I was getting 570Mb/s with and without the module when connected via the wire. While 1043ND is a gigabit router, it looks like 1 gigabit is shared among all the ports, thus each one got roughly half of what the backplane is capable of during my test.

It does not look like there is a built-in support for manipulating ebtables using uci, so for now I have a simple script that sets up the restrictions:

set -x

ebtables -t nat -P PREROUTING ACCEPT

ebtables -t nat -F port-eth0-1
ebtables -t nat -F PREROUTING

ebtables -t nat -X port-eth0-1

ebtables -t nat -N port-eth0-1

# My entries in /etc/ethers have the following format:
# de:ad:be:ef:00:12  eth.servername
# 01:02:03:04:05:06  wlan0.laptopname

for X in $(awk '/eth[0-9]?\./ { print $1 }' /etc/ethers); do
    ebtables -t nat -A port-eth0-1 -s $X -j RETURN

ebtables -t nat -A PREROUTING -i eth0.1 -j port-eth0-1

ebtables -t nat -P port-eth0-1 DROP