Tunnelling VLANs over Wi-Fi with OpenWrt and GRE

My home network is effectively in two segments, with the cable modem, router/server and access point wired together downstairs and another access point acting as a bridge, connected to a wired network in my office upstairs.

I'd been wanting to up my security game and split out dedicated networks (VLANs) and SSIDs for trusted devices, guests and untrusted IoT things for a while. One of the frustrating things about using Wi-Fi to bridge the two wired networks is that this typically precludes VLAN functionality.

I could just do things right and run a cable between the two wired islands, but my willingness to venture into technical escapades vastly exceeds my willingness to drill holes in walls and ceilings. And so, I spent a full day of my 2023 Christmas holiday messing around with OpenWrt.

All my routing is performed by my NUC server, so I'm using both OpenWrt devices purely as access points. Downstairs, I have a Ubiquiti Unifi 6 Long Range which was easy to reflash with vanilla OpenWrt by following the instructions on that link. Upstairs I have a Belkin RT3200, also flashed with vanilla OpenWrt. It turns out both devices are very similar, running the same SoC and Wi-Fi 6 chipsets. The Belkin unit has a 4 x 1GbE port switch + WAN port, where the Unifi has a single 2.5GbE uplink.

It is possible to pass VLANs over Wi-Fi by tunnelling your L2 network over IP, with the caveat of needing a higher than typically MTU to allow for the overhead. I created a mesh connection between the APs, bringing up network interfaces on either side with static IPs. Over that connection GRETAP devices on either end are able to pass Ethernet frames, and bridge to interfaces exposing endpoints for each VLAN.

I'm indebted to oofnikj for his post and more especially his config repo for getting me closer to a working configuration than the official docs.

If you need or want a similar setup, read on.

Tips

tcpdump was my friend on this adventure, revealing a storm of DHCP, ARP and multicast traffic at one point that was escaping the VLANs due to the switch in the Belkin unit effectively munging everything together and confusing the rest of the network until I got the configuration correct.

I started the configuration with both APs on the same wired network, next to each other. There came a point, just after bringing up the tunnel, where I had to make sure to disconnect one of them to avoid loopbacks. STP should take care of this, but didn't seem to for me, though that may have been down to the switch issue above.

Several times along the way I was saved by the dedicated tunnel network, where one AP was accessible, I could typically SSH from it using the internal IP assigned to the other to fix things.

My setup involves the trusted network behaving as the primary, untagged VLAN on each other physical Ethernet ports, with the IoT and Guest VLANs tagged.

Any feedback on what I could have done better is most welcome!

AP 1 Configuration (Unifi 6 LR, downstairs, attached to router)

# /etc/config/network

config interface 'loopback'
	option device 'lo'
	option proto 'static'
	option ipaddr '127.0.0.1'
	option netmask '255.0.0.0'

config globals 'globals'
	option ula_prefix 'fd92:bb11:b3e9::/48'

# I'm not clear on why this is a device rather than an interface
# but it was the default config, and it works, so I don't feel like
# experimenting with it further now!
config device
	option name 'br-lan'
	option type 'bridge'
	list ports 'eth0'
	# VLAN 1 on the interface connected to the GRE tunnel
	list ports '@trunk.1'
	option stp '1'
	option igmp_snooping '1'

# this interface holds the IP of the AP on the trusted network
config interface 'lan'
	option device 'br-lan'
	# I configure my IPs with fixed entries in DHCP
	option proto 'dhcp'
	# don't delegate IPv6, my router will take care of it
	option delegate '0'

# this interface is one end of the underlying IP network for the tunnel
# between the two APs
config interface 'wtun'
	option proto 'static'
	# any address outside our normal range is fine here
	option ipaddr '172.16.0.1'
	option netmask '255.255.255.0'
	option delegate '0'
	# it's here we need a larger-than-normal MTU to be able to pass
	# our 1500 MTU + overhead Ethernet frames
	option mtu '2048'

# the trunk bridge acts as the Ethernet endpoint of the GRE tunnel
# and we can create VLAN tagged versions of it to bridge to our
# wired and wireless interfaces
config interface 'trunk'
	option type 'bridge'
	option proto 'none'
	option bridge_empty '1'
	option delegate '0'
	option stp '1'
	option defaultroute '0'

# the GRE tunnel itself
config interface 'gre'
	option proto 'gretap'
	option ipaddr '172.16.0.1'
	option peeraddr '172.16.0.2'
	# transit over the dedicated network we created
	option tunlink 'wtun'
	# attach to our trunk bridge
	option network 'trunk'
	option df '0'
	option mtu '1500'
	option delegate '0'

# a bridge connecting the IoT (101) VLAN between the GRE
# tunnel and whatever we want to attach to it
config device
	option name 'br-iot'
	option type 'bridge'
	list ports 'br-lan.101'
	list ports '@trunk.101'
	option stp '1'
	option igmp_snooping '1'

# bring up an interface on the IoT VLAN for this AP
config interface 'iot'
	option proto 'dhcp'
	option device 'br-iot'
	option defaultroute '0'
	option peerdns '0'
	option delegate '0'

# a bridge connecting the Guest (102) VLAN between the
# GRE tunnel and whatever we want to attach to it
config device
	option name 'br-guest'
	option type 'bridge'
	list ports 'br-lan.102'
	list ports '@trunk.102'
	option stp '1'
	option igmp_snooping '1'

# bring up an interface on the Guest VLAN for this AP
config interface 'guest'
	option proto 'dhcp'
	option device 'br-guest'
	option defaultroute '0'
	option peerdns '0'
	option delegate '0'

AP2 Configuration (RT3200, upstairs)

# /etc/config/network

config interface 'loopback'
	option device 'lo'
	option proto 'static'
	option ipaddr '127.0.0.1'
	option netmask '255.0.0.0'

config globals 'globals'
	option ula_prefix 'fddd:12af:6646::/48'

config interface 'wtun'
	option proto 'static'
	option ipaddr '172.16.0.2'
	option netmask '255.255.255.0'
	option delegate '0'
	option mtu '2048'

config interface 'trunk'
	option type 'bridge'
	option proto 'none'
	option bridge_empty '1'
	option delegate '0'
	option stp '1'
	option defaultroute '0'

config interface 'gre'
	option proto 'gretap'
	option ipaddr '172.16.0.2'
	option peeraddr '172.16.0.1'
	option tunlink 'wtun'
	option network 'trunk'
	option df '0'
	option mtu '1500'

# my lan bridge is a little different on this AP due to the switch
# ports available on this device, which allow me to dedicate specific
# ports to VLANs as needed. Here every port is configured with the
# trusted network (VLAN 1) as it's untagged, primary VLAN and the
# other networks are available with tags on each port.
config device
	option name 'br-lan'
	option type 'bridge'
	option stp '1'
	option igmp_snooping '1'
	option bridge_empty '1'
	# attach VLAN 1 of the tunnelled Ethernet to this bridge, doesn't
	# seem quite right, but breaks without it
	list ports 'br-trunk.1'
	list ports 'lan1'
	list ports 'lan2'
	list ports 'lan3'
	list ports 'lan4'
	list ports 'wan'

# the untagged primary trusted VLAN on all ports
config bridge-vlan
	option device 'br-lan'
	option vlan '1'
	list ports 'br-trunk.1:u*'
	list ports 'lan1:u*'
	list ports 'lan2:u*'
	list ports 'lan3:u*'
	list ports 'lan4:u*'
	list ports 'wan:u*'

# tag the IoT VLAN on all ports
config bridge-vlan
	option device 'br-lan'
	option vlan '101'
	list ports 'lan1:t'
	list ports 'lan2:t'
	list ports 'lan3:t'
	list ports 'lan4:t'
	list ports 'wan:t'
	option local '0'

# tag the Guest VLAN on all ports
config bridge-vlan
	option device 'br-lan'
	option vlan '102'
	list ports 'lan1:t'
	list ports 'lan2:t'
	list ports 'lan3:t'
	list ports 'lan4:t'
	list ports 'wan:t'

# bring up an interface for the AP on the trusted VLAN
config interface 'lan'
	option device 'br-lan.1'
	option proto 'dhcp'
	option delegate '0'

config device
	option name 'br-iot'
	option type 'bridge'
	option stp '1'
	option igmp_snooping '1'
	list ports 'br-lan.101'
	list ports 'br-trunk.101'

config interface 'iot'
	option proto 'dhcp'
	option ipv6 '0'
	option device 'br-iot'
	option defaultroute '0'
	option peerdns '0'
	option delegate '0'

config device
	option name 'br-guest'
	option type 'bridge'
	option stp '1'
	option igmp_snooping '1'
	list ports 'br-lan.102'
	list ports 'br-trunk.102'

config interface 'guest'
	option proto 'dhcp'
	option ipv6 '0'
	option device 'br-guest'
	option defaultroute '0'
	option peerdns '0'
	option delegate '0'

Common configuration

The wireless and firewall configurations are identical on both APs. If your APs have different hardware, you'll likely need to adjust the radio sections for each.

# /etc/config/wireless
config wifi-device 'radio0'
	option type 'mac80211'
	option path 'platform/18000000.wmac'
	option channel '6'
	option band '2g'
	option htmode 'HT20'
	option cell_density '0'

config wifi-device 'radio1'
	option type 'mac80211'
	option phy 'wl1'
	option country 'US'
	option cell_density '0'
	option htmode 'HE80'
	option band '5g'
	option channel '149'

# the mesh network underlying our GRE tunnel
# I'm only using the 5GHz radio for this, because the signal is strong
# and the access points are static
config wifi-iface 'wifinet7'
	option device 'radio1'
	option mode 'mesh'
	option encryption 'sae'
	option mesh_id 'upstream'
	option mesh_fwding '1'
	option mesh_rssi_threshold '0'
	option key 'SomeRandomString'
	option network 'wtun'
	option ifname 'wtun'

# the 2GHz version of my trusted network SSID
config wifi-iface 'wifinet2'
	option device 'radio0'
	option mode 'ap'
	option ssid 'Trusted'
	option encryption 'sae'
	option key 'SuperStrongPassphrase'
	option network 'lan'

# the 5GHz version of my trusted network SSID
config wifi-iface 'wifinet5'
	option device 'radio1'
	option mode 'ap'
	option ssid 'Trusted'
	option encryption 'sae-mixed'
	option key 'SuperStrongPassphrase'
	option network 'lan'

# the 2GHz version of my Guest network SSID
config wifi-iface 'wifinet9'
	option device 'radio0'
	option mode 'ap'
	option ssid 'Guest'
	option encryption 'sae-mixed'
	# setting isolate to prevent wireless devices on this
	# SSID from talking to each other (same on IoT)
	option isolate '1'
	option key 'welcomeguest'
	option network 'guest'

# the 5GHz version of my Guest network SSID
config wifi-iface 'wifinet8'
	option device 'radio1'
	option mode 'ap'
	option ssid 'Guest'
	option encryption 'sae-mixed'
	option key 'letmeinplease'
	option network 'welcomeguest'
	option isolate '1'

# the 2GHz version of my IoT network SSID
config wifi-iface 'wifinet10'
	option device 'radio0'
	option mode 'ap'
	option ssid 'IoT'
	option encryption 'psk2'
	option isolate '1'
	option key 'untrusted'
	option network 'iot'

# the 5GHz version of our my network SSID
config wifi-iface 'wifinet11'
	option device 'radio1'
	option mode 'ap'
	option ssid 'IoT'
	option encryption 'psk2'
	option isolate '1'
	option key 'untrusted'
	option network 'iot'
# /etc/config/firewall
config defaults
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'REJECT'
	option synflood_protect '1'

# I have a firewall zone for each network
config zone
	option name 'lan'
	# only the trusted network and underlying tunnel network
	# have "input 'ACCEPT'" which will allow access to the
	# AP admin interfaces
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'lan'

config zone
	option name 'guest'
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'guest'

config zone
	option name 'iot'
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'iot'

config zone
	option name 'tunnel'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'wtun'