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.
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'/> </interface>
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 done ebtables -t nat -A PREROUTING -i eth0.1 -j port-eth0-1 ebtables -t nat -P port-eth0-1 DROP