Azure VPN Gateway Transit for Virtual Network Peering

Communication between VNets can be achieved by creating VNet peering between the two. However, it gets cumbersome when we have a growing number of VNets and have to keep adding mesh connections (any to any) between the VNets. The mesh connection occurs due to the fact that a VNet is not a transitive network where one VNet can use another VNet to reach another VNet.

If you have a small cloud infrastructure with only a few VNets, I wrote on how we could connect those VNets between each other via mesh connection. This could be achieved using the standard VNet peering or VNet Gateway peering if you want to have an encrypted tunnel.

This is actually the common challenge where more VNets will be deployed in the future and adding more connections is not the viable answer as it complicates the deployment and managing the peering. When I say common this just highlight one of the usual “Agile” approaches to making things happen in a short amount of time without thinking about the long term consequences. Granted I may have suggested a wrong view of Agile but let’s set that cultural problem aside and focus on the solution we could provide.

Azure actually allows creating a transit VNet where other VNets could just peer into it and use the transit VNet as a hub to reach another VNets. This model helps to reduce complexity and obviously cost (less peering, less money, and less paracetamol). This could be achieved by setting up a dedicated VNet as a “hub” site with VNet Gateway and enabling it to be a transit gateway (--allow-gateway-transit). In addition to that, we need to manually create additional routes (User Define Routes – UDR) so that the spoke VNet could see the other spoke’s routes.

Just a little note, if you come across this article VPN Gateway Peering Gateway Transit from Microsoft, make sure to read the UDR Requirements and constraints to complete the build.

This tutorial is rather lengthy but the approach is actually quite short using Azure CLI. I use only Azure CLI rather than web point-and-click approach cuts the building time shorter. It will take only 1 hour to create this lab and 30 minutes of it was just waiting for one VNet Gateway to be deployed.

See the lab diagram setup below.

We start with setting some of the recurring parameters. Please change the parameters below accordingly to suit your environment.

Note. There is another way to set these parameters by using the set.default command but I’m afraid that for some this could lead to a problem where engineers may have forgotten about it when moving to another deployment.

SubscriptionNameId=(Change this to your Subscription name)
ResourceGroupId=(Change this to your Resource Group name)
LocationId=(Change this to your Azure region)
LinuxAdminUsername=(Change this to your preferred username)
LinuxAdminPassword=(Change this to your preferred password)

Create the lab environment with three VNets and a subnet each. You don’t actually need a subnet created for this but subnets are created to host a Linux box to verify the routing Effective routes and facilitate the ICMP PING test.

  1. One vnet-hub with CIDR 10.0.0.0/16 with one subnet 10.0.1.0/24
  2. One vnet-spoke1 with CIDR 10.1.0.0/16 with one subnet 10.1.1.0/24
  3. One vnet-spoke2 with CIDR 10.2.0.0/16 with one subnet 10.1.1.0/24

az network vnet create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-hub \
--location australiaeast \
--address-prefixes 10.0.0.0/16 \
--subnet-name 10.0.1.0-24 \
--subnet-prefixes 10.0.1.0/24

az network vnet create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-spoke1 \
--location australiaeast \
--address-prefixes 10.1.0.0/16 \
--subnet-name 10.1.1.0-24 \
--subnet-prefixes 10.1.1.0/24

az network vnet create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-spoke2 \
--location australiaeast \
--address-prefixes 10.2.0.0/16 \
--subnet-name 10.2.1.0-24 \
--subnet-prefixes 10.2.1.0/24

Create Linux host in each of the created subnets.

  1. hub-linux1 in vnet-hub subnet 10.0.1.0/24
  2. spoke1-linux1 in vnet-spoke1 subnet 10.1.1.0/24
  3. spoke2-linux1 in vnet-spoke2 subnet 10.2.1.0/24

# Create a Linux host in subnet 10.0.1.0/24 vnet-hub.
az vm create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--vnet-name vnet-hub \
--name hub-linux1 \
--location $LocationId \
--image UbuntuLTS \
--authentication-type password \
--admin-username $LinuxAdminUsername \
--admin-password $LinuxAdminPassword \
--storage-sku Standard_LRS \
--size Standard_B1s \
--subnet 10.0.1.0-24 \
--subnet-address-prefix 10.0.1.0-24 \
--public-ip-sku Standard \
--no-wait

# Create a Linux host in subnet 10.1.1.0/24 vnet-spoke.
az vm create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--vnet-name vnet-spoke1 \
--name spoke1-linux1 \
--location $LocationId \
--image UbuntuLTS \
--authentication-type password \
--admin-username $LinuxAdminUsername \
--admin-password $LinuxAdminPassword \
--storage-sku Standard_LRS \
--size Standard_B1s \
--subnet 10.1.1.0-24 \
--subnet-address-prefix 10.1.1.0-24 \
--public-ip-sku Standard \
--no-wait

# Create a Linux host in subnet 10.2.1.0/24 vnet-spoke.
az vm create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--vnet-name vnet-spoke2 \
--name spoke2-linux1 \
--location $LocationId \
--image UbuntuLTS \
--authentication-type password \
--admin-username $LinuxAdminUsername \
--admin-password $LinuxAdminPassword \
--storage-sku Standard_LRS \
--size Standard_B1s \
--subnet 10.2.1.0-24 \
--subnet-address-prefix 10.2.1.0-24 \
--public-ip-sku Standard \
--no-wait

Once the VNets, subnets, and linux hosts are ready, we start the peering configuration by creating the VNet Gateway (name vnet-hub-gw\) in vnet-hub. There are three steps required to create VNet Gateway:

  1. Create a GatewaySubnet to host the vnet-hub-gw. We could use at minimum /29 or /27 range but for scale, it is recommended to have this in /27 range.
  2. Create a public IP vnet-hub-gw-public. Not that we will use it in this scenario but it’s just the requirement when creating a Vnet Gateway.
  3. Create VNet Gateway using the created GatewaySubnet and vnet-hub-gw-public.

# Create GatewaySubnet
az network vnet subnet create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--vnet-name vnet-hub \
--name GatewaySubnet \
--address-prefix 10.0.255.0/27

# Create Public IP
az network public-ip create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-hub-gw-public-ip \
--allocation-method Dynamic

# Create VNet Gateway
az network vnet-gateway create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-hub-gw \
--location australiaeast \
--gateway-type Vpn \
--vpn-type RouteBased \
--sku VpnGw2 \
--vpn-gateway-generation Generation2 \
--vnet vnet-hub \
--public-ip-addresses vnet-hub-gw-public-ip \
--no-wait

Create Linux host in each of the created subnets.

  1. hub-linux1 in vnet-hub subnet 10.0.1.0/24
  2. spoke1-linux1 in vnet-spoke1 subnet 10.1.1.0/24
  3. spoke2-linux1 in vnet-spoke2 subnet 10.2.1.0/24

# Create a Linux host in subnet 10.0.1.0/24 vnet-hub.
az vm create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--vnet-name vnet-hub \
--name hub-linux1 \
--location $LocationId \
--image UbuntuLTS \
--authentication-type password \
--admin-username $LinuxAdminUsername \
--admin-password $LinuxAdminPassword \
--storage-sku Standard_LRS \
--size Standard_B1s \
--subnet 10.0.1.0-24 \
--subnet-address-prefix 10.0.1.0-24 \
--public-ip-sku Standard \
--no-wait

# Create a Linux host in subnet 10.1.1.0/24 vnet-spoke.
az vm create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--vnet-name vnet-spoke1 \
--name spoke1-linux1 \
--location $LocationId \
--image UbuntuLTS \
--authentication-type password \
--admin-username $LinuxAdminUsername \
--admin-password $LinuxAdminPassword \
--storage-sku Standard_LRS \
--size Standard_B1s \
--subnet 10.1.1.0-24 \
--subnet-address-prefix 10.1.1.0-24 \
--public-ip-sku Standard \
--no-wait

# Create a Linux host in subnet 10.2.1.0/24 vnet-spoke.
az vm create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--vnet-name vnet-spoke2 \
--name spoke2-linux1 \
--location $LocationId \
--image UbuntuLTS \
--authentication-type password \
--admin-username $LinuxAdminUsername \
--admin-password $LinuxAdminPassword \
--storage-sku Standard_LRS \
--size Standard_B1s \
--subnet 10.2.1.0-24 \
--subnet-address-prefix 10.2.1.0-24 \
--public-ip-sku Standard \
--no-wait

Once the VNets, subnets, and Linux hosts are ready, we start the peering configuration by creating the VNet Gateway (name vnet-hub-gw\) in vnet-hub. There are three steps required to create VNet Gateway:

  1. Create a GatewaySubnet to host the vnet-hub-gw. We could use at minimum /29 or /27 range but for scale, it is recommended to have this in /27 range.
  2. Create a public IP vnet-hub-gw-public. Not that we will use it in this scenario but it’s just the requirement when creating a Vnet Gateway.
  3. Create VNet Gateway using the created GatewaySubnet and vnet-hub-gw-public.

# Create GatewaySubnet
az network vnet subnet create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--vnet-name vnet-hub \
--name GatewaySubnet \
--address-prefix 10.0.255.0/27

# Create Public IP
az network public-ip create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-hub-gw-public-ip \
--allocation-method Dynamic

# Create VNet Gateway
az network vnet-gateway create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-hub-gw \
--location australiaeast \
--gateway-type Vpn \
--vpn-type RouteBased \
--sku VpnGw2 \
--vpn-gateway-generation Generation2 \
--vnet vnet-hub \
--public-ip-addresses vnet-hub-gw-public-ip \
--no-wait

Make sure that the VNet Gateway are ProvisioningState is Succeeded before proceeding to the next step. The creating of VNet Gateway could take up more than 20 minutes.

az network vnet-gateway show -g $ResourceGroupId -n vnet-hub-gw | grep provisioningState

Once the VNet Gateway is created, we could start building the peering between VNets. The peering is not bidirectional and it means that you will need to create two peerings for each connection.

  1. Create peering between vnet-hub to vnet-spoke1 (named hub-to-spoke1), and also create peering between vnet-spoke1 to vnet-hub (named spoke1-to-hub).
  2. The same with peering to Spoke2, create peering between vnet-hub to vnet-spoke2 (named hub-to-spoke2), and also create peering between vnet-spoke2 to vnet-hub (named spoke2-to-hub).

On both peering hub-to-spoke1 and hub-to-spoke2, make sure to enable --allow-forwarded-traffic, --allow-gateway-transit, and --allow-vnet-access. The option --allow-gateway-transit will make this VNet Gateway as a transit gateway to allow communication between spokes.

On both peering spoke1-to-hub and spoke2-to-hub, make sure to enable --allow-forwarded-traffic, --allow-vnet-access, and --use-remote-gateways. The option --use-remote-gateways will make these spoke1 and spoke2 to use the vnet-hub-gw as a transit.

# Get the id for vnet-hub
vnethubid=$(az network vnet show \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-hub \
--query id --out tsv)

# Get the id for vnet-spoke1
vnetspoke1id=$(az network vnet show \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-spoke1 \
--query id --out tsv)

# Get the id for vnet-spoke2
vnetspoke2id=$(az network vnet show \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name vnet-spoke2 \
--query id --out tsv)

# Create hub-to-spoke1 peering
az network vnet peering create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name hub-to-spoke1 \
--allow-forwarded-traffic \
--allow-gateway-transit \
--allow-vnet-access \
--vnet-name vnet-hub \
--remote-vnet $vnetspoke1id

# Create spoke1-to-hub peering
az network vnet peering create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name spoke1-to-hub \
--allow-forwarded-traffic \
--allow-vnet-access \
--vnet-name vnet-spoke1 \
--remote-vnet $vnethubid \
--use-remote-gateways

# Create hub-to-spoke2 peering
az network vnet peering create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name hub-to-spoke2 \
--allow-forwarded-traffic \
--allow-gateway-transit \
--allow-vnet-access \
--vnet-name vnet-hub \
--remote-vnet $vnetspoke2id

# Create spoke2-to-hub peering
az network vnet peering create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--name spoke2-to-hub \
--allow-vnet-access \
--vnet-name vnet-spoke2 \
--remote-vnet $vnethubid \
--use-remote-gateways

Now that VNet Gateway and peerings are created, let’s check on the Effective routes for each of Linux hosts on each VNets.

# Get the Linux NIC name for hub-linux1
HubLinux1NICId=$(az vm show -n hub-linux1 -g $ResourceGroupId --query \
'[networkProfile.networkInterfaces][].id' -otsv )

# Get the hub-linux1 Effective routes.
az network nic show-effective-route-table --id $HubLinux1NICId -o table

# Get the Linux NIC name for spoke1-linux1
Spoke1Linux1NICId=$(az vm show -n spoke1-linux1 -g $ResourceGroupId --query \
'[networkProfile.networkInterfaces][].id' -otsv )

# Get the spoke1-linux1 Effective routes.
az network nic show-effective-route-table --id $Spoke1Linux1NICId -o table

# Get the Linux NIC name for spoke2-linux1
Spoke2Linux1NICId=$(az vm show -n spoke2-linux1 -g $ResourceGroupId --query \
'[networkProfile.networkInterfaces][].id' -otsv )

# Get the spoke2-linux1 Effective routes.
az network nic show-effective-route-table --id $Spoke2Linux1NICId -o table

You should see that the hub-linux1 NIC has both 10.1.0.0/16 (vnet-spoke1) and 10.2.0.0/16 (vnet-spoke2), including its own 10.0.0.0/16. However, for spoke1, it only sees route 10.0.0.0/16 (vnet-hub) and its own 10.1.0.0/16 but not 10.2.0.0/16 (vnet-spoke2). This means that vnet-spoke1 does not know the route to get to vnet-spoke2. Also, for spoke2, it only sees route 10.0.0.0/16 (vnet-hub) and its own 10.2.0.0/16 but not 10.1.0.0/16 (vnet-spoke1). This also means that vnet-spoke2 does not know the route to get to vnet-spoke1 either.

After posting this question to Microsoft Azure Community, I’ve been advised (Thank You!) to add these routes manually via something called User Defined Routing (UDR) to vnet-spoke1 and vnet-spoke2.

The suggestion from Microsoft below.

In addition to forwarding traffic to an on-premises network, a VPN gateway can forward network traffic between virtual networks that are peered with the virtual network the gateway is in, without the virtual networks needing to be peered with each other. Using a VPN gateway to forward traffic is useful when you want to use a VPN gateway in a hub (see the hub and spoke example described for Allow forwarded traffic) virtual network to route traffic between spoke virtual networks that aren’t peered with each other. To learn more about allowing use of a gateway for transit, see Configure a VPN gateway for transit in a virtual network peering. This scenario requires implementing user-defined routes that specify the virtual network gateway as the next hop type. Learn about user-defined routes. You can only specify a VPN gateway as a next hop type in a user-defined route, you can’t specify an ExpressRoute gateway as the next hop type in a user-defined route.

So, let’s create the required UDR.

Just a little kind note to self. I was creating the Route table using az network route-table route create but it keeps giving me an error that the route table name is not exist. It turned out that I had to use az network route-table create instead (without the word route).

# Create route table for Spoke1
az network route-table create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--location $LocationId \
--name spoke1-udr

# Create route entry for Spoke1-UDR
az network route-table route create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--route-table-name spoke1-udr \
--name spoke1-to-spoke2-via-gw-route1 \
--next-hop-type VirtualNetworkGateway \
--address-prefix 10.2.0.0/16

# Associate route table spoke1-udr to vnet-spoke1 subnet 10.1.1.0-24
az network vnet subnet update \
--resource-group $ResourceGroupId \
--vnet-name vnet-spoke1 \
--route-table spoke1-udr \
--name 10.1.1.0-24

# Create route table for Spoke2
az network route-table create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--location $LocationId \
--name spoke2-udr

# Create route entry for Spoke2-UDR
az network route-table route create \
--subscription $SubscriptionNameId \
--resource-group $ResourceGroupId \
--route-table-name spoke2-udr \
--name spoke2-to-spoke1-via-gw-route1 \
--next-hop-type VirtualNetworkGateway \
--address-prefix 10.1.0.0/16

# Associate route table spoke2-udr to vnet-spoke2 subnet 10.2.1.0-24
az network vnet subnet update \
--resource-group $ResourceGroupId \
--vnet-name vnet-spoke2 \
--route-table spoke2-udr \
--name 10.2.1.0-24

Now let’s check the routes in both Spoke1-Linux1 and Spoke2-Linux2. It should be showing each other’s routes.

# Get the Linux NIC name for hub-linux1
HubLinux1NICId=$(az vm show -n hub-linux1 -g $ResourceGroupId --query \
'[networkProfile.networkInterfaces][].id' -otsv )

# Get the hub-linux1 Effective routes.
az network nic show-effective-route-table --id $HubLinux1NICId -o table

# Get the Linux NIC name for spoke1-linux1
Spoke1Linux1NICId=$(az vm show -n spoke1-linux1 -g $ResourceGroupId --query \
'[networkProfile.networkInterfaces][].id' -otsv )

# Get the spoke1-linux1 Effective routes.
az network nic show-effective-route-table --id $Spoke1Linux1NICId -o table

# Get the Linux NIC name for spoke2-linux1
Spoke2Linux1NICId=$(az vm show -n spoke2-linux1 -g $ResourceGroupId --query \
'[networkProfile.networkInterfaces][].id' -otsv )

# Get the spoke2-linux1 Effective routes.
az network nic show-effective-route-table --id $Spoke2Linux1NICId -o table

See below for the expected correct output.

david@Azure:~$ # Get the Linux NIC name for hub-linux1
david@Azure:~$ HubLinux1NICId=$(az vm show -n hub-linux1 -g $ResourceGroupId --query \
> '[networkProfile.networkInterfaces][].id' -otsv )

david@Azure:~$ # Get the hub-linux1 Effective routes.
david@Azure:~$ az network nic show-effective-route-table --id $HubLinux1NICId -o table
Source State Address Prefix Next Hop Type Next Hop IP
-------- ------- ---------------- --------------- -------------
Default Active 10.0.0.0/16 VnetLocal
Default Active 10.1.0.0/16 VNetPeering
Default Active 10.2.0.0/16 VNetPeering
Default Active 0.0.0.0/0 Internet
Default Active 10.0.0.0/8 None
(omitted for brevity...)


david@Azure:~$ # Get the Linux NIC name for spoke1-linux1
david@Azure:~$ Spoke1Linux1NICId=$(az vm show -n spoke1-linux1 -g $ResourceGroupId --query \
> '[networkProfile.networkInterfaces][].id' -otsv )

david@Azure:~$ # Get the spoke1-linux1 Effective routes.
david@Azure:~$ az network nic show-effective-route-table --id $Spoke1Linux1NICId -o table
Source State Address Prefix Next Hop Type Next Hop IP
-------- ------- ---------------- --------------------- -------------
Default Active 10.1.0.0/16 VnetLocal
Default Active 10.0.0.0/16 VNetPeering
User Active 10.2.0.0/16 VirtualNetworkGateway
Default Active 0.0.0.0/0 Internet
Default Active 10.0.0.0/8 None
(omitted for brevity...)


david@Azure:~$ # Get the Linux NIC name for spoke2-linux1
david@Azure:~$ Spoke2Linux1NICId=$(az vm show -n spoke2-linux1 -g $ResourceGroupId --query \
> '[networkProfile.networkInterfaces][].id' -otsv )

david@Azure:~$ # Get the spoke2-linux1 Effective routes.
david@Azure:~$ az network nic show-effective-route-table --id $Spoke2Linux1NICId -o table
Source State Address Prefix Next Hop Type Next Hop IP
-------- ------- ---------------- --------------------- -------------
Default Active 10.2.0.0/16 VnetLocal
Default Active 10.0.0.0/16 VNetPeering
User Active 10.1.0.0/16 VirtualNetworkGateway
Default Active 0.0.0.0/0 Internet
Default Active 10.0.0.0/8 None
(omitted for brevity...)

Let’s SSH to the Linux box in Spoke1 and you could see that it can now PING Linux in Hub and Spoke2.

linuxuser36826@spoke1-linux1:~$ ping 10.1.1.4 -c 3
PING 10.1.1.4 (10.1.1.4) 56(84) bytes of data.
64 bytes from 10.1.1.4: icmp_seq=1 ttl=64 time=0.035 ms
64 bytes from 10.1.1.4: icmp_seq=2 ttl=64 time=0.052 ms
64 bytes from 10.1.1.4: icmp_seq=3 ttl=64 time=0.042 ms

--- 10.1.1.4 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2041ms
rtt min/avg/max/mdev = 0.035/0.043/0.052/0.007 ms
linuxuser36826@spoke1-linux1:~$ ping 10.0.1.4 -c 3
PING 10.0.1.4 (10.0.1.4) 56(84) bytes of data.
64 bytes from 10.0.1.4: icmp_seq=1 ttl=64 time=1.81 ms
64 bytes from 10.0.1.4: icmp_seq=2 ttl=64 time=1.53 ms
64 bytes from 10.0.1.4: icmp_seq=3 ttl=64 time=1.27 ms

--- 10.0.1.4 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.271/1.536/1.808/0.219 ms
linuxuser36826@spoke1-linux1:~$ ping 10.2.1.4 -c 3
PING 10.2.1.4 (10.2.1.4) 56(84) bytes of data.
64 bytes from 10.2.1.4: icmp_seq=1 ttl=63 time=3.99 ms
64 bytes from 10.2.1.4: icmp_seq=2 ttl=63 time=4.49 ms
64 bytes from 10.2.1.4: icmp_seq=3 ttl=63 time=5.33 ms

--- 10.2.1.4 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 3.988/4.602/5.329/0.553 ms

Additional resources are below.

VPN Gateway Peering Gateway Transit
Create, change, or delete a virtual network peering – Requirements and constraints
Get Started with Azure CLI
az network route table route
az network route-table update

One thought on “Azure VPN Gateway Transit for Virtual Network Peering

Leave a comment