Skip to content

Instantly share code, notes, and snippets.

Created December 31, 2023 20:58
Show Gist options
  • Save amkgi/78c3b6256b400fcdea856518eaa0460e to your computer and use it in GitHub Desktop.
Save amkgi/78c3b6256b400fcdea856518eaa0460e to your computer and use it in GitHub Desktop.
L2 NAT for a VLAN interface with a wireless parent interface bridged to the VMs in macOS

IEEE: 802.11 (Wi-Fi), 802.1Q (VLAN tagging); bridges and VMs in macOS Sonoma

If for some reason you need to tag traffic outgoing from a vNICs of VMs bridged to a wireless interface in macOS, you can do so as follows.

I will describe this process with VLAN configuration on an OpenWrt router with DSA support.

Configuring the router:

Open the interfaces tab on the Network menu in LuCI. Switch to the Devices tab and click configure br-lan, then switch to Bridge VLAN filtering and configure as in the screenshot below.


Do not apply these settings immediately. After enabling VLAN filtering in the bridge, we need to change the lan interface device from br-lan to br-lan.1 on the Interfaces tab. After making these changes to the lan interface, the configuration can be applied.

After that you need to add a VLAN tag for the wireless interface, this cannot be done via LuCI and you need to SSH into the router. Install the ip-bridge package and run the following command afterwards:

bridge vlan add dev phy0-ap0 vid 5

You can verify that the interface is tagged with the following command:

bridge vlan show vid 5
port              vlan-id
lan1              5 PVID Egress Untagged
phy1-ap0          5

Configuring VLAN and VM in macOS:

Create a VLAN interface and assign a VID to it, specifying the wireless interface as the parent interface:

sudo ifconfig vlan5 create
sudo ifconfig vlan5 vlan 5 vlandev en0

To create VMs I use virt-install with passing arguments to configure network interfaces via qemu:commandline because vmnet-bridged, vmnet-shared, vmnet-host libvirt still doesn't support. You can use lima-vm or UTM, VMware Fusion. But in the case of the latter two, they will not allow you to explicitly specify a VLAN interface in bridge mode when creating a VM. You can remove the Wi-Fi interface in bridge mode after creating the VM and add the one you want using the following commands:

sudo ifconfig bridge100 deletem en0
sudo ifconfig bridge100 addm vlan5

Command to create a VM with virt-install:

sudo virt-install \
	--name test-vm \
	--memory 4096 \
	--vcpus 2 \
	--disk=size=20,backing_store="/var/lib/libvirt/images/CentOS-Stream-GenericCloud-9-latest.aarch64.qcow2" \
	--disk path=/var/lib/libvirt/boot/cidata.iso,device=cdrom \
	--import \
	--os-variant centos-stream9 \
	--graphic vnc \
	--noautoconsole \
	--virt-type hvf \
	--qemu-commandline='-netdev vmnet-bridged,id=net0,ifname=vlan5 -device virtio-net-device,netdev=net0' \
	--network user
--disk path=/var/lib/libvirt/boot/cidata.iso,device=cdrom

This disk image contains meta-data and user-data files for cloud-init. It can be made with the following commands:

mkdir cidata
cat > cidata/user-data << EOF
password: qwerty123
chpasswd: { expire: False }
ssh_pwauth: True

cat > cidata/meta-data << EOF
instance-id: test-vm
local-hostname: test-vm

hdiutil makehybrid -o /var/lib/libvirt/boot/cidata.iso cidata -iso -joliet

I specify to create the vNIC in bridge mode with VLAN interface via qemu:commandline.

After creating a VM, a network bridge will appear in macOS interfaces, but you will find that packet transmission to your VM doesn't work properly. The reason is that the created VM will send packets through the router with its own MAC address, and the router won't know where to send the reply packets when the MAC address of your VM is in the frame, because the MAC address of your Wi-Fi interface in macOS will be different, and even if you add your VM's MAC address to the FDB table by specifying the router's wireless interface in OpenWrt, it won't help, because when sending a packet through the wireless interface the router doesn't look in the FDB and searches for the station's MAC address (STA) to send the packet to.

For example, your VM sends a broadcast ARP request and the host you are looking for will reply to it, the packet is sent to your VM's MAC address and will not be delivered because the VM's MAC address is not listed in the associated stations (Fig. 1). But at the same time your VM will reply to ARP requests from other hosts on the network and those host will receive a reply (Fig. 2).

Fig. 1
Fig. 2

To solve this issue you need MAC NAT, and it's easy to do. I found in the XNU kernel code and ifconfig utility that this can be done, but it is not described in the documentation or in the ifconfig man pages and if it wasn't for this issue I wouldn't have noticed that if a vNIC is bridged with a wireless interface, this flag is set automatically, so I haven't encountered this issue before until I needed the VLANs.

As a confirmation, this listing shows that the flag is set automatically when the wireless interface is added to the network bridge:

	if (wifi_infra) {
		(void)bridge_mac_nat_enable(sc, bif);

But in the case of a bridge connection to a VLAN interface there is no condition in the code to set this flag and as I wrote above ifconfig allows you to set this flag:

sudo ifconfig bridge100 macnat vlan5

Verify the flag is set and that MAC learning has happened:

ifconfig -v bridge100
bridge100: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1496 index 24
	ether f2:2f:4b:c0:7c:64
		id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0
		maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
		root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0
		ipfilter disabled flags 0x0
	member: vlan5 flags=8003<LEARNING,DISCOVER,MACNAT> <<--
	        ifmaxaddr 0 port 27 priority 0 path cost 0
	        hostfilter 0 hw: 0:0:0:0:0:0 ip:
	member: vmenet0 flags=3<LEARNING,DISCOVER>
	        ifmaxaddr 0 port 23 priority 0 path cost 0
	        hostfilter 0 hw: 0:0:0:0:0:0 ip:
	Address cache:
		10:e7:c6:xx:xx:xx Vlan1 vlan5 1199 flags=0<>
		fa:16:3e:c9:25:bf Vlan1 vmenet0 1199 flags=0<>
		52:54:0:12:34:56 Vlan1 vmenet0 1191 flags=0<>
	MAC NAT list: <<--
		vmenet0 52:54:0:12:34:56 1191 <<--
	media: autoselect
	status: active
	generation id: 137
	state availability: 0 (true)
	qosmarking enabled: yes mode: none
	low power mode: disabled
	multi layer packet logging (mpklog): disabled
	routermode4: disabled
	routermode6: disabled

Now the packet capture will show the MAC address of your wireless interface and the packet will be sent to the MacBook's wireless interface, then to the network bridge and here the destination MAC header will be replaced in the frame:


But keep in mind that MAC learning won't be happen if you bridged the wireless interface with vNIC and assign a VLAN tag inside the VM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment