Bridging Networks with Windows Packet Filter

By | November 14, 2016

Why do I need to bridge or a couple of real life cases when you may need an alternative to built-in Windows network bridge?

Over ten years have passed since I have published the first version of Ethernet Bridge. The main purpose of this simple tool, inspired by Steve Gibson from Gibson Research Corporation, was supporting OpenVPN in bridging mode on Windows 2000. Native OS support for network bridging was introduced in Windows XP/2003, and I had not seen much sense to port Ethernet Bridge to more modern operating systems. However, growing interest in cloud computing and virtualization discovered some new applications where built-in Windows network bridging support can be insufficient.

Let’s look at one such a case. Assume that we have two laptops connected with Ethernet switch or direct cable. One of the laptops is also connected to Wi-Fi, and we want to share this Wi-Fi connection for the second laptop (Figure 1).

Two laptops sharing one Wi Fi connection
Figure 1

Of course, we can use built-in Windows Internet connection sharing or any other NAT-based solution like Internet Gateway. But what if we want to have the second laptop in the same LAN segment? And this is where built-in Network Bridging may help. We just bridge LAN and WLAN connections on the first laptop and get the second laptop magically connected to the same LAN segment (Figure 2 and Figure 3):

Bridging Networks in Windows
Figure 2

As you may notice on the Figure 3 below, instead of two network interfaces, you virtually get a single one connected to both LAN and Wi-Fi simultaneously.

Windows built-in Network Bridge activated
Figure 3

So far so good, but as you may already know Windows 8 and later client Windows systems (as well as Windows Server 2008 and later Windows server systems) include a Hyper-V  hypervisor which can create guest virtual machines. Besides other details, to get your virtual machine connected to the network, you have to create a so-called virtual switch. On the Figure 4 below, I have created a virtual switch connected to the Wi-Fi network interface. As a result, Windows has created a virtual network interface vEthernet(WLAN) where WLAN stands for the name of the Hyper-V virtual switch and which is connected to the wireless network.

Windows Hyper-V virtual switch
Figure 4

What if I try to bridge Ethernet and vEthernet adapters now? Of course, I would expect it to work the same way as in the previous case, where we had no Hyper-V virtual switches and my second laptop would get connected to the Wi-Fi LAN segment.

Bridging with Hyper-V virtual switch
Figure 5

Windows agrees to add an Ethernet adapter to the bridge (Figure 5) but instead of having a second laptop connected, I have lost network connectivity on the first laptop and could restore it only after removing Hyper-V virtual switch from the system. In this particular case, it means that I can’t have virtual machines running under Hyper-V connected to Wi-Fi and at the same time have my second laptop bridged to a Wi-Fi connection.

Another possible situation when Windows built-in network bridging support can be insufficient:

  • When one or more virtual networks interfaces (like VirtNet) have to be bridged to the real network, each having a dedicated IP address.
  • Special packet filters have to be applied before packets from one bridged segment goes to another for security reasons (bridging firewall).
  • Or you are just curious on how you could build your network bridge for Windows :-).

Building network bridge on top of Windows Packet Filter

The complete source code for this project is available on GitHub. You can just download it from there, build and run. Please note that this application requires Windows Packet Filter driver installed. You can download it from the official web-page. Here I’m going to provide some general overview and highlight important details. And the first thing to note is that there  are two slightly different cases: when you want to bridge two Ethernet network adapters and when you bridge a Wi-Fi connection.

Ethernet to Ethernet bridge

This looks pretty straightforward.  Just put both network interfaces into the promiscuous mode and duplicate packets from the first network interface to the second and vice versa. For the Ethernet Bridge sample application, I create two working threads. Each thread is filtering on the primary network interface and duplicates packets to the secondary. The code below is fairly simple, it does not try to prevent network loops by using Spanning Tree protocol, which you may consider implementing when building a production level Ethernet Bridge. Just tries to avoid some unnecessary packets indications to avoid loopback loops. The code fragment below is the main processing cycle of the working thread:

while (eBridgePtr->m_bIsRunning)
{
	Adapters[First]->WaitEvent(INFINITE);
 
	// Reset event, as we don't need to wake up all working threads at once
 
	if (eBridgePtr->m_bIsRunning)
		Adapters[First]->ResetEvent();
 
	// Start reading packet from the driver
 
	while (eBridgePtr->ReadPackets(ReadRequest))
	{

		for (size_t i = 0; i < ReadRequest->dwPacketsSuccess; ++i)
		{
 
			if (PacketBuffer[i].m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
			{
				// For outgoing packets add to list only originated from the current
				// interface (to skip possible loopback indications)
				if(Adapters[First]->IsLocal(PacketBuffer[i].m_IBuffer + ETH_ALEN))
				{
					BridgeRequest->EthPacket[BridgeRequest->dwPacketsNumber].Buffer = &PacketBuffer[i];
					++BridgeRequest->dwPacketsNumber;
				}
			}
			else
			{
				// For incoming packets don't add to list packets destined to local
				// interface (they are not supposed to be bridged anythere else)
				if(!Adapters[First]->IsLocal(PacketBuffer[i].m_IBuffer))
				{
					BridgeRequest->EthPacket[BridgeRequest->dwPacketsNumber].Buffer = &PacketBuffer[i];
					++BridgeRequest->dwPacketsNumber;
				} 
			}

			// For local indications add only directed or broadcast/multicast
			if ((PacketBuffer[i].m_IBuffer[0] & 0x01)
				|| Adapters[Second]->IsLocal(PacketBuffer[i].m_IBuffer)
				)
			{
				MstcpBridgeRequest->EthPacket[MstcpBridgeRequest->dwPacketsNumber].Buffer = &PacketBuffer[i];
				++MstcpBridgeRequest->dwPacketsNumber;
			}
		}
 
		if (BridgeRequest->dwPacketsNumber)
		{
			eBridgePtr->SendPacketsToAdapter(BridgeRequest);
			BridgeRequest->dwPacketsNumber = 0;
		}

		if (MstcpBridgeRequest->dwPacketsNumber)
		{
			eBridgePtr->SendPacketsToMstcp(MstcpBridgeRequest);
			MstcpBridgeRequest->dwPacketsNumber = 0;
		}
 
		ReadRequest->dwPacketsSuccess = 0;
	}
}

So far, so good and if you are to bridge a couple of Ethernet network interfaces then this is sufficient.

Ethernet to Wi-Fi bridge

The code above does Ethernet bridging transparently (doesn’t modify outgoing or incoming frames) but it won’t work when you need to bridge wired and wireless networks because most Access Points (APs) will reject frames that have a source address that didn’t authenticate with the AP. The solution is MAC address (also known as physical or hardware address) translation between wired LAN and Wi-Fi:

  • For packets going from LAN to Wi-Fi, we change the source MAC address in the Ethernet header to the wireless network card MAC address. So, the wireless Access Point will accept such frames.
  • For packets going from Wi-Fi to LAN we need a reverse operation, e.g. we have to change destination MAC address before forwarding it to the network. But how do we know what MAC address to use? To solve this problem, we require a table matching IP address to MAC address. Using this table, we can find out the MAC address by IP address extracted from the packet headers.

Here is the resulted code:

while (eBridgePtr->m_bIsRunning)
{
	Adapters[First]->WaitEvent(INFINITE);
 
	// Reset event, as we don't need to wake up all working threads at once
 
	if (eBridgePtr->m_bIsRunning)
		Adapters[First]->ResetEvent();
 
	// Start reading packet from the driver
 
	while (eBridgePtr->ReadPackets(ReadRequest))
	{
		//
		// WLAN requires MAC NAT
		//
		if (Adapters[First]->IsWLAN())
		{
			// Process packets from WLAN:
			// Need to lookup correct MAC address for each packet by its IP address
			// and replace destination MAC address
			for (size_t i = 0; i < ReadRequest->dwPacketsNumber; ++i)
			{
				ether_header_ptr pEtherHdr = reinterpret_cast(ReadRequest->EthPacket[i].Buffer->m_IBuffer);
				if (ntohs(pEtherHdr->h_proto) == ETH_P_IP)
				{
					iphdr_ptr pIpHdr = (iphdr*)(ReadRequest->EthPacket[i].Buffer->m_IBuffer + ETHER_HEADER_LENGTH);
						
					auto dest_mac = Adapters[First]->GetMacByIp(pIpHdr->ip_dst);
					if (!(dest_mac == mac_address::empty))
					{
						memcpy(pEtherHdr->h_dest, &dest_mac[0], ETH_ALEN);
					}
				}

				if (ntohs(pEtherHdr->h_proto) == ETH_P_ARP)
				{
					ether_arp_ptr pArpHdr = reinterpret_cast(ReadRequest->EthPacket[i].Buffer->m_IBuffer + ETHER_HEADER_LENGTH);

					if (ntohs(pArpHdr->ea_hdr.ar_op) == ARPOP_REQUEST)
					{

					}
					else
					{
						auto dest_mac = Adapters[First]->GetMacByIp(*reinterpret_cast<in_addr*>(pArpHdr->arp_tpa));
						if (!(dest_mac == mac_address::empty))
						{
							memcpy(pEtherHdr->h_dest, &dest_mac[0], ETH_ALEN);
							memcpy(pArpHdr->arp_tha, &dest_mac[0], ETH_ALEN);
						}
					}
				}
			}
		}

		if (Adapters[Second]->IsWLAN())
		{
			// Process packets to WLAN:
			// Need to change source MAC to WLAN adapter MAC 
			// and save pair IP->MAC for the future
			for (size_t i = 0; i < ReadRequest->dwPacketsNumber; ++i)
			{
				ether_header_ptr pEtherHdr = reinterpret_cast(ReadRequest->EthPacket[i].Buffer->m_IBuffer);

				//
				// ARP processing. Here we save pairs of IP and MAC addresses for future use
				//
				if (ntohs(pEtherHdr->h_proto) == ETH_P_ARP)
				{
					ether_arp_ptr pArpHdr = reinterpret_cast(ReadRequest->EthPacket[i].Buffer->m_IBuffer + ETHER_HEADER_LENGTH);

					if (ntohs(pArpHdr->ea_hdr.ar_op) == ARPOP_REQUEST)
					{
						// ARP request
							
						// Save pair of IP and MAC
						Adapters[Second]->SetMacForIp(
							*reinterpret_cast<in_addr*>(pArpHdr->arp_spa), 
							&pArpHdr->arp_sha[0]
							);

						// Replace source MAC in ARP request to WLAN adapter one
						memmove(&pArpHdr->arp_sha[0], &Adapters[Second]->GetHwAddress()[0], ETH_ALEN);
					}
					else
					{
						// ARP reply
							
						// Save pair of IP and MAC
						Adapters[Second]->SetMacForIp(
							*reinterpret_cast<in_addr*>(pArpHdr->arp_spa), 
							&pArpHdr->arp_sha[0]
							);

						// Replace target MAC in ARP request to WLAN adapter one
						memmove(&pArpHdr->arp_sha[0], &Adapters[Second]->GetHwAddress()[0], ETH_ALEN);
					}

				}

				//
				// DHCP requests preprocessing (there is no sense to send UNICAST DHCP requests if we use MAC NAT)
				//
				if (ntohs(pEtherHdr->h_proto) == ETH_P_IP)
				{
					iphdr_ptr pIpHeader = reinterpret_cast(ReadRequest->EthPacket[i].Buffer->m_IBuffer + ETHER_HEADER_LENGTH);

					if (pIpHeader->ip_p == IPPROTO_UDP)
					{
						udphdr_ptr pUdpHeader = reinterpret_cast(((PUCHAR)pIpHeader) + sizeof(DWORD)*pIpHeader->ip_hl);
						if (ntohs(pUdpHeader->th_dport) == IPPORT_DHCPS)
						{
							dhcp_packet* pDhcp = reinterpret_cast<dhcp_packet*>(pUdpHeader + 1);
								
							if ((pDhcp->op == BOOTREQUEST) &&
								(pDhcp->flags == 0)
								)
							{
								// Change DHCP flags to broadcast 
								pDhcp->flags = htons(0x8000);
								RecalculateUDPChecksum(ReadRequest->EthPacket[i].Buffer);
								RecalculateIPChecksum(pIpHeader);
							}

						}
					}
				}

				// Replace source MAC in Ethernet header
				memmove(&pEtherHdr->h_source, &Adapters[Second]->GetHwAddress()[0], ETH_ALEN);

				// Mark packet as MAC NAT applied
				ReadRequest->EthPacket[i].Buffer->m_Reserved[0] = 1;
			}
		}
 
		for (size_t i = 0; i < ReadRequest->dwPacketsSuccess; ++i)
		{
 
			if (PacketBuffer[i].m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
			{
				// For outgoing packets add to list only orginated from the current interface (to skip possible loopback indications)
				if(Adapters[First]->IsLocal(PacketBuffer[i].m_IBuffer + ETH_ALEN)||
					(Adapters[Second]->IsLocal(PacketBuffer[i].m_IBuffer + ETH_ALEN)))
				{
					BridgeRequest->EthPacket[BridgeRequest->dwPacketsNumber].Buffer = &PacketBuffer[i];
					++BridgeRequest->dwPacketsNumber;
				}
			}
			else
			{
				// For incoming packets don't add to list packets destined to local interface (they are not supposed to be bridged anythere else)
				if(!Adapters[First]->IsLocal(PacketBuffer[i].m_IBuffer))
				{
					BridgeRequest->EthPacket[BridgeRequest->dwPacketsNumber].Buffer = &PacketBuffer[i];
					++BridgeRequest->dwPacketsNumber;
				} 
			}

			// For local indications add only directed or broadcast/multicast
			if ((PacketBuffer[i].m_IBuffer[0] & 0x01)
				|| Adapters[Second]->IsLocal(PacketBuffer[i].m_IBuffer)
				)
			{
				MstcpBridgeRequest->EthPacket[MstcpBridgeRequest->dwPacketsNumber].Buffer = &PacketBuffer[i];
				++MstcpBridgeRequest->dwPacketsNumber;
			}
		}
 
		if (BridgeRequest->dwPacketsNumber)
		{
			eBridgePtr->SendPacketsToAdapter(BridgeRequest);
			BridgeRequest->dwPacketsNumber = 0;
		}

		if (MstcpBridgeRequest->dwPacketsNumber)
		{
			eBridgePtr->SendPacketsToMstcp(MstcpBridgeRequest);
			MstcpBridgeRequest->dwPacketsNumber = 0;
		}
 
		ReadRequest->dwPacketsSuccess = 0;
	}
}

As you may already have noticed, this code has some special processing for ARP and DHCP protocols. And probably it needs some explanation:
ARP (Address Resolution Protocol) is used for resolution of Internet layer addresses into link layer addresses. Assuming that a peer with IP address 192.168.1.11 wants to communicate to 192.168.1.1, it asks using ARP protocol:”Who has 192.168.1.1? Tell 192.168.1.11!” and broadcasts this packet on the network. And when 192.168.1.1 receives such a request, it replies to 192.168.1.11 with an ARP reply telling its MAC address.
Since ARP is used for mapping a network address (e.g. an IPv4 address) to a physical hardware address like an Ethernet address (also named a MAC address) and we translate MAC addresses between LAN and Wi-Fi then we also need to properly adjust information transferred by ARP protocol as the following:

  • For packets destined from LAN to Wi-Fi, we:
    1. save the pairs of IP and hardware address for our IP-to-MAC table
    2. replace hardware address in the ARP requests/replies to wireless network card address
  • For ARP replies destined from Wi-Fi to LAN, we look up hardware MAC by IP address (using the IP-to-MAC table built in previous step) and use the resulted hardware address as a replacement for destination hardware MAC in both Ethernet header and ARP reply body

The Dynamic Host Configuration Protocol (DHCP) is a standardized network protocol used on Internet Protocol (IP) networks to dynamically distributes network configuration parameters, such as IP addresses, for interfaces and services. In a few words, when your laptop connects to Wi-Fi, the DHCP server in the Wi-Fi network (typically running on a Wi-Fi access point or router) assigns your laptop an IP address, configures the default router and DNS servers. To allow hosts in LAN to obtain IP addresses using DHCP, we normally forward their DHCP requests to Wi-Fi network. The small block of DHCP related code above converts unicast DHCP requests to broadcast ones because in case of unicast requests DHCP server will reply to the physical address of the requester directly what does not make sense since this physical address is behind the Wi-Fi adapter and DHCP response will never reach the requester. Although this block of code can be safely omitted because if the system fails to get and IP address with unicast DHCP request then it switches to broadcast after a small timeout. So, we just speed up the things a little.

Bridging VirtNet to Wi-Fi

I have already mentioned above that we can use Ethernet Bridge to bridge virtual network interfaces to the real network. On the screenshot below, VirtNet network interface is bridged to the Wi-Fi network. As you can see, it has a correct DHCP-assigned IP address and can be operated just as a normal physical network interface.

Bridging VirtNet to Wi-Fi
Figure 6

6 thoughts on “Bridging Networks with Windows Packet Filter

  1. Cosmin

    Congrats for this nice pice pf software!
    I still have a problem though with iSCSI boot..
    I have a Win 2012 which has the C:\ drive as iSCSI disk and it’s using iPXE for iSCSI boot.
    Installing Winpk filter I’m not able to boot. Console shows INACCESSIBLE BOOT DEVICE.

    Reply
    1. Vadim Smirnov

      Do you mean that Windows 2012 with remote boot is unable to find the boot device after installing WinpkFilter driver? I think you understand that WinpkFilter driver loads as a part of the Windows (not a part of BIOS or UEFI) and it can’t do anything before Windows is started. The problem must be in something else.

      If there is a different configuration, like iSCSI is behind WinpkFilter bridge then it may have sense and the situation really worth analyses.

      Reply
      1. Cosmin

        Thanks for your reply, actually the boot error happens after the BIOS stage completion.
        It’s a Windows error. As I can’t upload the console screenshot, find bellow what the screen displays:

        “Your PC ran into a problem and needs to restart. We’re just collecting some error info, and then we’ll restart for you. (0% complete). If you’d like to know more, you can search online later for this error: INACCESSIBLE BOOT DEVICE”.

        Booting from local disk and uninstalling the Winpk driver will make the iSCSI boot to work.

        Reply
  2. Sparky

    The link to the Github repository does not work anymore, could I please have the link if it still exists?

    Reply

Leave a Reply to Sparky Cancel reply

Your email address will not be published. Required fields are marked *