Build a Multi-Subnet Routed Network
Route traffic between Azure subnets through a Linux NVA using custom UDRs.
Introduction
30 Second Summary
Every time you visit a website, your data doesn't teleport directly to its destination. It hops through routers that read addresses and decide which path each packet takes next.
In this project, you will build a multi-subnet network in Azure and configure a Linux virtual machine to act as a router that forces traffic through it. You'll create custom routes, watch packets traverse your router using traceroute, and intentionally break routing to practice real-world troubleshooting.
What You'll Build
Picture yourself SSHing into a virtual machine, running traceroute, and watching your packet hop through a custom Linux router before landing at its destination in a completely different subnet.
By the end of this project, you'll have:
- A working network with three subnets (Public, DMZ, Private) where you can trace a packet's full path from source to destination and see each intermediate hop.
- A Linux Network Virtual Appliance acting as your router, proving its presence as a visible hop when you run traceroute between VMs.
- A routing troubleshooting lab where you break configurations on purpose, diagnose failures with real network tools, and restore connectivity.
- Secret Mission: Force traffic through your router in both directions and verify with a live HTTP request end-to-end.
Do I need any prerequisites?
This project assumes you have an Azure account and are comfortable running basic Linux terminal commands. No prior Azure networking experience is required.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. This project asks you to think like a network engineer, not just click through a tutorial.
Set Up Azure CLI and Create Your Lab Environment
To build a multi-subnet network with custom routing, you need a way to talk to Azure from your terminal. The Azure CLI lets you create networks, VMs, and route tables with single commands instead of clicking through the portal.
In this step, you will install the Azure CLI, log in, and create a dedicated resource group that keeps all your lab resources in one place for easy cleanup later.
In this step, get ready to:
- Install and verify the Azure CLI on macOS.
- Log in to your Azure account.
- Create a resource group for the lab.
Install and verify the Azure CLI
The Azure CLI is a command-line tool that lets you create and manage Azure resources directly from your terminal. On macOS, the easiest way to install it is through Homebrew.
- Check whether the Azure CLI is already installed by running this command in your terminal:
az version
✔️ I see a version number
You are all set. The Azure CLI is installed and ready to use.
ⓧ Command not found
The Azure CLI is not installed yet. Install it with Homebrew by running these commands:
brew update && brew install azure-cli
This updates your Homebrew package list and installs the Azure CLI. The install may take a minute or two.
- After the install completes, verify it worked by running:
az version
You should see a JSON block showing the installed Azure CLI version.
Still seeing command not found?
Close your terminal window and open a new one so the shell picks up the new az command.
If Homebrew itself is not installed, visit brew.sh and follow the one-line install command first, then retry the Azure CLI install.
help me install the Azure CLI on macOS
Log in to your Azure account
Before you can create any resources, the CLI needs to authenticate against your Azure subscription.
- Log in to Azure by running this command:
az login
What happens when you run this?
On macOS, az login opens your default browser and loads the Azure sign-in page. Sign in with the same account you use for the Azure portal.
Once you authenticate in the browser, your terminal displays a JSON list of your subscriptions. The subscription marked isDefault: true is the one your CLI commands will use.
- Sign in with your Azure account credentials in the browser window that opens.
- Return to your terminal and confirm you see a JSON list of subscriptions.
Browser not opening?
If no browser window appears, the CLI falls back to device code flow and shows a URL with a code. Open the URL manually, enter the code, and complete sign-in there.
You can also force device code flow directly by running az login --use-device-code.
help me log in to Azure CLI
Create a resource group for the lab
A resource group is a container that holds related Azure resources. Putting everything in one group means you can delete the entire lab with a single command when you are done.
- Create your lab resource group by running this command:
az group create --name routing-lab-rg --location eastus2
What does this command do?
- az group create creates a new resource group in your Azure subscription.
- --name routing-lab-rg sets the name. You will reference this name in every command for the rest of the project.
- --location eastus2 places the group in the East US 2 Azure region, which has all the services this lab needs.
You should see a JSON response with "provisioningState": "Succeeded" confirming the group was created.
Before you run the verification command, what do you expect to see when you query this resource group?
- Verify the resource group exists and confirm your subscription by running:
az group show --name routing-lab-rg
You should see the full resource group details with "location": "eastus2" and your subscription ID in the id field.
Seeing 'resource group not found'?
Make sure you typed the name exactly as routing-lab-rg with no extra characters. Resource group names are case-insensitive but must match exactly otherwise.
If you have multiple subscriptions, confirm the active one is correct by running az account show and checking the subscription name. Switch subscriptions with az account set --subscription followed by the subscription name or ID.
help me find my resource group
Your lab environment is ready. Next up, you will build the virtual network and deploy VMs into your three subnets.
Build the VNet, Subnets, and Deploy VMs
Before you can control how traffic flows between subnets, you need a network to route through. Azure automatically routes traffic between subnets within the same VNet using system routes.
In this step, you'll build the network from scratch and confirm that Azure gives you "free" connectivity between all subnets. This baseline is the behaviour you'll override with custom routes in the next step.
In this step, get ready to:
- Create a VNet with three subnets (Public, DMZ, Private).
- Deploy three Ubuntu VMs, one in each subnet.
- Prove all VMs can reach each other through Azure's default routing.
Create a VNet with three subnets
A Virtual Network (VNet) is the private address space your VMs live in. You'll carve it into three subnets, each representing a different network zone.
- Create the VNet and its first subnet by running this command:
az network vnet create \
--name lab-vnet \
--resource-group routing-lab-rg \
--address-prefixes 10.0.0.0/16 \
--subnet-name subnet-public \
--subnet-prefixes 10.0.1.0/24
What does this command do?
- Creates a VNet named lab-vnet with the full 10.0.0.0/16 address space (65,536 addresses).
- Carves out the first subnet subnet-public using 10.0.1.0/24 (256 addresses).
- Add the DMZ subnet by running:
az network vnet subnet create \
--name subnet-dmz \
--resource-group routing-lab-rg \
--vnet-name lab-vnet \
--address-prefixes 10.0.2.0/24
- Add the private subnet by running:
az network vnet subnet create \
--name subnet-private \
--resource-group routing-lab-rg \
--vnet-name lab-vnet \
--address-prefixes 10.0.3.0/24
- Confirm your three subnets exist by running:
az network vnet subnet list \
--resource-group routing-lab-rg \
--vnet-name lab-vnet \
--output table
You should see three rows: subnet-public, subnet-dmz, and subnet-private.
Why three subnets?
This layout mirrors a real production network: a public-facing zone, a DMZ for middleboxes (firewalls, routers), and a private zone for sensitive workloads. Your NVA router will sit in the DMZ and forward traffic between the other two zones.
Not seeing all three subnets?
- Make sure you used --vnet-name lab-vnet in the subnet create commands. A typo here creates the subnet in a different (or non-existent) VNet.
- Confirm the resource group matches: --resource-group routing-lab-rg.
Why aren't my subnets showing up?
Deploy three Ubuntu VMs
Each VM needs to land in a specific subnet with a public IP so you can SSH in. For the NVA VM, you'll create its NIC explicitly so it has a predictable name you can reference later when enabling IP forwarding.
- Create the NVA's network interface by running:
az network nic create \
--name vm-nva-nic \
--resource-group routing-lab-rg \
--vnet-name lab-vnet \
--subnet subnet-dmz
Why create the NIC separately?
Later, you'll enable IP forwarding on this NIC by referencing its name. Creating it explicitly with the name vm-nva-nic gives you a predictable handle instead of relying on Azure's auto-generated name.
- Deploy vm-public in subnet-public by running:
az vm create \
--name vm-public \
--resource-group routing-lab-rg \
--image Ubuntu2404 \
--size Standard_B1s \
--vnet-name lab-vnet \
--subnet subnet-public \
--nsg-rule SSH \
--admin-username azureuser \
--generate-ssh-keys
This takes 1-2 minutes. When it finishes, the CLI prints a JSON object including the VM's publicIpAddress.
- Deploy vm-nva in subnet-dmz using the NIC you just created:
az vm create \
--name vm-nva \
--resource-group routing-lab-rg \
--image Ubuntu2404 \
--size Standard_B1s \
--nics vm-nva-nic \
--admin-username azureuser \
--generate-ssh-keys
What about the NSG for vm-nva?
When you use --nics to attach a pre-created NIC, az vm create does not auto-create an NSG. You'll SSH into vm-nva from within the VNet (from vm-public) later. All intra-VNet traffic is allowed by default, so no additional NSG rule is needed for that path.
- Deploy vm-private in subnet-private by running:
az vm create \
--name vm-private \
--resource-group routing-lab-rg \
--image Ubuntu2404 \
--size Standard_B1s \
--vnet-name lab-vnet \
--subnet subnet-private \
--nsg-rule SSH \
--admin-username azureuser \
--generate-ssh-keys
- Confirm all three VMs are running by listing them:
az vm list \
--resource-group routing-lab-rg \
--output table \
--show-details
You should see three rows with PowerState showing VM running for each. Note down the PublicIps column for vm-public.
VM creation failed?
- If you see a quota error, your subscription may have vCPU limits in eastus2. Try a different region or request a quota increase from the Azure portal.
- If you see a size availability error for Standard_B1s, the B1s SKU may not be available in your region. Try Standard_B2s as an alternative.
Help me fix my Azure VM creation error.
SSH in and test default routing
Azure's system routes let subnets within the same VNet talk to each other automatically. Let's prove this baseline works before you override it.
- Get the public IP of vm-public by running:
az vm show \
--name vm-public \
--resource-group routing-lab-rg \
--show-details \
--query publicIps \
--output tsv
- SSH into vm-public using the IP from the output:
ssh azureuser@[[VM_PUBLIC_IP="the public IP from the previous command"]]
First time connecting?
You'll see a fingerprint prompt asking if you want to continue connecting. Type yes and press Enter to accept.
- Install traceroute on vm-public by running:
sudo apt-get install -y traceroute
Before you test, think about what you expect to see. Since Azure routes all intra-VNet traffic directly, how many hops should appear between vm-public and vm-private?
- Ping vm-private to confirm basic connectivity:
ping -c 3 10.0.3.4
You should see three replies from 10.0.3.4 with 0% packet loss.
- Run traceroute to see the packet path:
traceroute 10.0.3.4
You should see a single hop. Azure's implicit router sends the packet directly from subnet-public to subnet-private with no intermediate device.
What does this prove?
This baseline shows that Azure gives you free, direct connectivity between all subnets in a VNet. In the next step, you'll override this default by creating a custom route table that forces traffic through your NVA instead.
Ping or traceroute not working?
- Make sure you're running the commands from inside vm-public (your prompt should show azureuser@vm-public).
- Confirm vm-private is running. In a separate terminal, run az vm show --name vm-private --resource-group routing-lab-rg --show-details --query powerState --output tsv and check it shows VM running.
- If traceroute shows only asterisks, wait a minute and retry. The VM may still be booting.
Help me debug why ping to 10.0.3.4 is failing from vm-public.
Your three-subnet lab network is live and all VMs can talk to each other. Next up, you'll create a custom route table that forces traffic through your NVA, breaking this direct path on purpose.
Create a Route Table and Force Traffic Through the NVA
In the previous step, you confirmed that Azure routes traffic between subnets automatically through system routes. In traditional networking, traffic between segments passes through a router or firewall for inspection and control.
In this step, you'll create a User Defined Route that overrides this default behavior. Traffic from subnet-public destined for subnet-private will be forced through your NVA instead of going directly.
In this step, get ready to:
- Create a route table and add a custom route pointing to the NVA.
- Associate the route table with subnet-public.
- Test the new routing path and observe how it changes traffic flow.
Create the route table and add a route
A route table is a container for routing rules. You'll create one and add a rule that tells Azure to send traffic destined for subnet-private to your NVA's IP address instead of using the default direct path.
- Create the route table by running this command:
az network route-table create --name route-table-public --resource-group routing-lab-rg --location eastus2
You'll see JSON output confirming the route table was created in your resource group.
- Add a route to the table that redirects traffic destined for subnet-private through the NVA by running:
az network route-table route create --route-table-name route-table-public --resource-group routing-lab-rg --name to-private-subnet --address-prefix 10.0.3.0/24 --next-hop-type VirtualAppliance --next-hop-ip-address 10.0.2.4
What does this route do?
This route says: any traffic headed for 10.0.3.0/24 (subnet-private) should not go directly. Instead, send it to 10.0.2.4 (your NVA in the DMZ subnet).
The next hop type VirtualAppliance tells Azure the destination is a VM acting as a network device, not a gateway or the internet. This is identical to configuring a static route on a physical router.
Seeing a 'resource not found' error?
Make sure you spelled route-table-public and routing-lab-rg exactly as shown. The route table must exist before you can add routes to it.
If the route table create command failed, confirm your subscription is active by running az account show.
Help me troubleshoot this error
Associate the route table with subnet-public
A route table only takes effect when it's associated with a subnet. Without association, the routing rules you just created are ignored and Azure continues using its default system routes.
- Attach the route table to subnet-public by running:
az network vnet subnet update --name subnet-public --vnet-name lab-vnet --resource-group routing-lab-rg --route-table route-table-public
You'll see JSON output showing the subnet configuration with the route table now linked.
What does association mean?
Associating a route table with a subnet tells Azure to evaluate those routes for all traffic leaving VMs in that subnet. The route table only affects outbound traffic from the associated subnet, not inbound traffic arriving at it.
Command failing with 'subnet not found'?
Confirm your VNet is named lab-vnet and the subnet is named subnet-public. Both names are case-sensitive.
Help me fix this subnet association error
Test the new routing path
Your route table is now active. Traffic from vm-public to subnet-private is being redirected to the NVA at 10.0.2.4.
- SSH into vm-public using the same credentials from Step 2:
ssh [[VM_PUBLIC_USER="your admin username"]]@[[VM_PUBLIC_IP="vm-public's public IP address"]]
Before you run the next command, think about what you expect to happen. In Step 2, pinging vm-private worked perfectly. Now that traffic is being redirected to the NVA, do you think the ping will still succeed?
- Test connectivity to vm-private by running:
ping -c 3 10.0.3.4
The ping times out with 100% packet loss. This is the expected result.
Why did the ping fail?
Your UDR is working correctly. Traffic from vm-public is no longer going directly to vm-private. It's being sent to the NVA at 10.0.2.4 instead.
The problem is that the NVA receives these packets but drops them. IP forwarding is disabled by default at both the Azure NIC layer and the Linux kernel layer. Without IP forwarding, a VM discards any packet that isn't addressed to its own IP.
This is the exact behavior you'd see with a misconfigured physical router. You'll fix this in the next step by enabling IP forwarding at both levels.
Ping is still succeeding?
If you still get replies from 10.0.3.4, the route table association may not have applied correctly. Exit your SSH session and re-run the subnet update command from the previous substep.
Also confirm you associated the route table with subnet-public (where vm-public lives), not one of the other subnets.
Help me figure out why my route table is not taking effect
Your route table is working. Traffic is being redirected to the NVA, but the NVA doesn't know what to do with it yet. Next up, you'll enable IP forwarding to turn the NVA into a functional router.
Enable IP Forwarding and Restore Connectivity
In the last step, you proved that your route table is working. Packets from vm-public destined for subnet-private are now being redirected to the NVA at 10.0.2.4. The problem is that those packets arrive and get silently dropped.
IP forwarding is disabled at two independent levels on your NVA. Azure's NIC layer drops any packet whose destination IP doesn't match the NIC's own address. The Linux kernel does the same thing independently. You need to flip both switches to turn your VM into a functioning router.
In this step, get ready to:
- Enable IP forwarding on the NVA's Azure NIC.
- Enable IP forwarding in the NVA's Linux kernel.
- Verify the routed path with traceroute.
Enable IP forwarding on the NVA's Azure NIC
The first gate is at the Azure networking layer. By default, Azure drops any traffic received by a NIC that isn't destined for that NIC's own IP address. This protects VMs from processing stray traffic, but it also prevents a VM from acting as a router.
- If you're still connected to an SSH session from the previous step, type exit to return to your local terminal.
- Enable IP forwarding on the NVA's NIC by running this command:
az network nic update --name vm-nva-nic --resource-group routing-lab-rg --ip-forwarding true
What does this command do?
- --name vm-nva-nic targets the specific network interface attached to your NVA.
- --ip-forwarding true tells Azure to stop dropping packets that aren't addressed to this NIC. The NIC now passes through traffic destined for other hosts.
- Look at the JSON output. Find the enableIPForwarding field and confirm it shows true.
Command failed with a 'not found' error?
- Double-check that --name is exactly vm-nva-nic and --resource-group is exactly routing-lab-rg. NIC names are case-sensitive.
- Make sure you exited any SSH session and are running this from your local terminal where Azure CLI is installed.
Help me troubleshoot why my az network nic update command is failing with a not-found error.
That's one gate open. Azure will now let forwarded packets pass through the NIC.
Enable IP forwarding in the Linux kernel on vm-nva
Even with Azure allowing the packets through, the Linux operating system has its own independent check. By default, the kernel discards any packet not addressed to one of its own IP addresses. You need to tell the kernel to forward those packets instead of dropping them.
- Get the public IP of vm-nva by running:
az vm show --show-details --resource-group routing-lab-rg --name vm-nva --query publicIps --output tsv
- SSH into vm-nva using the public IP from the output:
ssh azureuser@[[NVA_PUBLIC_IP="the public IP from the previous command"]]
- Enable IP forwarding in the Linux kernel by running:
sudo sysctl -w net.ipv4.ip_forward=1
What does this command do?
sysctl modifies kernel parameters at runtime. The net.ipv4.ip_forward parameter controls whether the kernel forwards packets between network interfaces. Setting it to 1 turns your VM into a packet forwarder.
This change is not persistent across reboots. In a production environment, you would write it to a config file. For this lab, the runtime setting is sufficient.
- Verify the setting took effect by running:
sysctl net.ipv4.ip_forward
- Confirm the output reads net.ipv4.ip_forward = 1.
Seeing 'permission denied'?
- Make sure you included sudo before the sysctl -w command. Writing kernel parameters requires root privileges.
- The verification command (without -w) does not need sudo because it only reads the value.
Help me fix a permission denied error when running sysctl on my Azure VM.
Both gates are now open. Your NVA can receive packets destined for other hosts and forward them onward.
Verify the routed path from vm-public
Both levels of IP forwarding are now enabled. It's time to test whether packets from vm-public actually traverse the NVA on their way to vm-private.
- Exit your SSH session on vm-nva by typing exit.
- Get the public IP of vm-public by running:
az vm show --show-details --resource-group routing-lab-rg --name vm-public --query publicIps --output tsv
- SSH into vm-public:
ssh azureuser@[[PUBLIC_VM_IP="the public IP from the previous command"]]
Before you run the next command, think about what you expect to see. In Step 2, traceroute showed a single direct hop. Now that your route table forces traffic through the NVA, how many hops should appear?
- Run traceroute to vm-private:
traceroute 10.0.3.4
You should see two hops. The first hop is 10.0.2.4 (your NVA in subnet-dmz). The second hop is 10.0.3.4 (vm-private). This proves that packets are traveling through your NVA router instead of taking Azure's default direct path.
- Confirm full connectivity is restored by running:
ping -c 3 10.0.3.4
- Verify you see 3 packets transmitted with 0% packet loss.
Why two levels of IP forwarding?
Azure's NIC-level forwarding and the Linux kernel's forwarding are completely independent controls. Azure added the NIC-level check as a safety measure so that even if a VM's OS is misconfigured, stray traffic won't leak between subnets.
In production, a Network Virtual Appliance would also run firewall rules, deep packet inspection, or logging between the forwarding decision and the actual send. Your NVA is the simplest version of this pattern: receive, forward, done.
Still seeing packet loss or only one hop?
- If ping times out, go back and verify both IP forwarding settings. Run az network nic show --name vm-nva-nic --resource-group routing-lab-rg --query enableIPForwarding from your local terminal to check the Azure layer.
- If traceroute shows only one hop (direct path), the route table may not be associated with subnet-public. Run az network vnet subnet show --name subnet-public --vnet-name lab-vnet --resource-group routing-lab-rg --query routeTable.id to confirm the association.
- If traceroute hangs on the first hop with * * *, SSH into vm-nva and re-run sysctl net.ipv4.ip_forward to confirm the kernel setting is still 1.
Help me troubleshoot why traceroute is not showing my NVA as a hop.
Your NVA is now a fully functioning router. Traffic from subnet-public to subnet-private flows through it, and you can prove it with traceroute. Next up, you'll intentionally break this setup in two different ways and practice diagnosing routing failures like a real network engineer.
Routing Troubleshooting Lab
In the last step, you proved that traffic flows through your NVA by reading the traceroute output.
Real network engineers spend most of their time diagnosing why traffic is not flowing. In this step, you will intentionally break your routing configuration in two different ways, diagnose each failure using the same tools a network engineer would use, and fix them.
In this step, get ready to:
- Misconfigure a route's next-hop IP and diagnose the failure.
- Disassociate a route table and observe how Azure reverts to default routing.
- Inspect effective routes on a NIC to see how Azure merges system routes and UDRs.
Break 1: Point traffic to a non-existent next hop
The first failure mode you will simulate is a wrong next-hop address. This is one of the most common routing misconfigurations in production networks.
- Change the route's next-hop IP to a non-existent address by running this command:
az network route-table route update \
--resource-group routing-lab-rg \
--route-table-name route-table-public \
--name to-private-subnet \
--next-hop-ip-address 10.0.2.99
What does this command do?
You are updating the route named to-private-subnet so that it points traffic at 10.0.2.99 instead of your NVA's real IP (10.0.2.4). No VM exists at that address, so packets sent there will be silently dropped.
Before you test, think about what will happen. The route table still forces traffic toward a VirtualAppliance next hop, but the IP it points to does not exist.
- SSH into vm-public (using the same SSH command from earlier steps).
- What do you expect to see when you ping vm-private? Form your prediction, then test it:
ping -c 3 10.0.3.4
The ping times out with 100% packet loss. The UDR redirects packets to 10.0.2.99, but nothing lives there to forward them onward.
Still seeing successful pings?
Azure may take a few seconds to propagate route changes. Wait 10-15 seconds and try the ping again. If it still succeeds, exit the SSH session and reconnect to vm-public to ensure the route has taken effect.
help me debug why my ping still succeeds after changing the next-hop IP to a non-existent address
Now diagnose the problem the way a network engineer would. Exit the SSH session back to your local terminal.
- Inspect the route to identify the bad next-hop by running:
az network route-table route show \
--resource-group routing-lab-rg \
--route-table-name route-table-public \
--name to-private-subnet \
--output table
In the output, the NextHopIpAddress field shows 10.0.2.99. That is the problem. The real NVA lives at 10.0.2.4.
- Fix the route by updating the next-hop back to the correct NVA IP:
az network route-table route update \
--resource-group routing-lab-rg \
--route-table-name route-table-public \
--name to-private-subnet \
--next-hop-ip-address 10.0.2.4
- SSH back into vm-public and verify the fix:
ping -c 3 10.0.3.4
Ping succeeds again. You diagnosed the wrong next-hop and restored connectivity.
Break 2: Remove the route table association from the subnet
The second failure mode is subtler. Instead of pointing to the wrong place, you will remove the route table from the subnet entirely. Azure will silently revert to its default direct routing.
- Exit your SSH session back to your local terminal.
- Disassociate the route table from subnet-public by running:
az network vnet subnet update \
--resource-group routing-lab-rg \
--vnet-name lab-vnet \
--name subnet-public \
--route-table ""
What does this command do?
Setting --route-table to an empty string detaches the route table from the subnet. The route table still exists, but it no longer affects traffic leaving subnet-public.
What do you think will happen now? The UDR is gone, so Azure falls back to its default system routes. Predict whether ping will succeed or fail, and whether the NVA will still be in the path.
- SSH into vm-public and test connectivity:
ping -c 3 10.0.3.4
Ping succeeds. But that does not mean your configuration is correct.
- Run traceroute to inspect the actual path:
traceroute 10.0.3.4
Traceroute shows only one hop, reaching vm-private directly. Your NVA (10.0.2.4) is completely bypassed. Azure reverted to default direct routing between subnets.
Why is this dangerous in production?
In a real environment, traffic would skip your firewall or inspection appliance without any error. Everything appears to work, but your security controls are silently bypassed.
This is why traceroute (not just ping) is essential for verifying routing paths.
- Exit the SSH session and reassociate the route table with subnet-public:
az network vnet subnet update \
--resource-group routing-lab-rg \
--vnet-name lab-vnet \
--name subnet-public \
--route-table route-table-public
- SSH back into vm-public and confirm the NVA hop has returned:
traceroute 10.0.3.4
You should see two hops again: first 10.0.2.4 (your NVA), then 10.0.3.4 (vm-private). Traffic is flowing through the NVA once more.
NVA hop not showing up?
Wait 15-20 seconds for the route table association to propagate, then re-run traceroute 10.0.3.4. If you still see only one hop, verify the association took effect:
az network vnet subnet show \
--resource-group routing-lab-rg \
--vnet-name lab-vnet \
--name subnet-public \
--query routeTable.id \
--output tsv
The output should contain route-table-public. If it is empty, re-run the association command above.
help me debug why my route table association is not taking effect on subnet-public
View effective routes to see how Azure merges system routes and UDRs
Azure maintains a combined routing table for every NIC, merging system routes (auto-generated) with any User Defined Routes you associate. You can inspect this merged view directly from the CLI.
- Exit the SSH session and view the effective routes on vm-public's NIC by running:
az network nic show-effective-route-table \
--resource-group routing-lab-rg \
--name vm-public-nic \
--output table
What does this output show?
Each row is a route that Azure considers when deciding where to send a packet from this NIC. The Source column tells you where each route came from.
Routes with source Default are system routes Azure creates automatically for intra-VNet traffic. Your UDR shows source User with destination 10.0.3.0/24 and next hop 10.0.2.4.
When a UDR and a system route cover the same destination prefix, the UDR wins. The overridden system route appears with state Invalid. This is how Azure enforces your custom routing decisions.
Seeing an error about the NIC name?
If you get a "not found" error for vm-public-nic, list the NICs in your resource group to find the correct name:
az network nic list \
--resource-group routing-lab-rg \
--output table
Look for the NIC attached to vm-public and substitute its name in the command above.
help me find the NIC name for my vm-public VM so I can view its effective routes
Before you wrap up, confirm everything is back to its correct working state.
- SSH into vm-public one final time and run the end-of-step verification:
traceroute 10.0.3.4
You see two hops: 10.0.2.4 (NVA) then 10.0.3.4 (vm-private).
- Confirm ping also succeeds:
ping -c 3 10.0.3.4
All three packets return successfully. Your routing is fully restored after both troubleshooting exercises.
You now have hands-on experience diagnosing the two most common routing failures in Azure networking. Next up, you will take on the Secret Mission and build bidirectional forced routing with HTTP verification.
Secret mission
Bidirectional Forced Routing with HTTP Verification
Right now, traffic from vm-public to vm-private flows through your NVA. But what about the return path? Traffic from vm-private back to vm-public still takes Azure's default direct route, completely bypassing your router. Can you make routing truly bidirectional and prove it with a real HTTP request?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running or delete them entirely. The three VMs in this project cost approximately $0.03/hour combined, so leaving them running will accrue charges.
Cost warning
Your three B1s VMs cost approximately $0.01/hour each ($0.03/hour total). If you walk away without cleaning up, you'll see charges accumulate on your Azure subscription.
The recommended option is to delete the entire resource group once you're done. This removes all resources instantly.
Resources you used:
- 1 resource group (routing-lab-rg).
- 1 VNet (lab-vnet) with 3 subnets.
- 3 Ubuntu VMs (B1s), each with a public IP, NIC, and OS disk.
- 2 route tables (route-table-public and route-table-private).
- 1 NSG allowing SSH.
Keep everything running
No action needed. Choose this if you're still actively experimenting with routes, testing new route table configurations, or planning to extend the lab further.
- Your VMs will continue running and accruing charges (~$0.03/hour total).
- All route tables, associations, and IP forwarding settings remain intact.
- You can SSH back into any VM at any time using its public IP.
Pause - I'll come back to this later
Deallocate your VMs to stop compute charges while keeping all your network configuration (VNet, subnets, route tables, associations) in place. You can restart the VMs later and pick up where you left off.
- Deallocate all three VMs by running these commands:
az vm deallocate --resource-group routing-lab-rg --name vm-public
az vm deallocate --resource-group routing-lab-rg --name vm-nva
az vm deallocate --resource-group routing-lab-rg --name vm-private
What does deallocate mean?
Deallocating releases the underlying compute hardware. This stops all compute charges. Your disks, network interfaces, public IPs, route tables, and VNet remain intact and cost very little to keep.
When you're ready to resume, run az vm start --resource-group routing-lab-rg --name <vm-name> for each VM.
- Note that the Linux kernel IP forwarding setting on vm-nva is not persistent across reboots. When you restart the VMs, re-enable it with sudo sysctl -w net.ipv4.ip_forward=1 inside vm-nva.
Delete - I don't want to use this again
Remove all project resources and start fresh. Deleting the resource group destroys everything inside it: VMs, disks, NICs, public IPs, VNet, subnets, route tables, and the NSG.
- Delete the entire resource group by running:
az group delete --name routing-lab-rg --yes --no-wait
What does this command do?
The --yes flag skips the confirmation prompt. The --no-wait flag returns control to your terminal immediately while Azure deletes resources in the background.
Deletion typically completes within 2-5 minutes. You can confirm by running az group list --output table and verifying routing-lab-rg no longer appears.
- After deletion completes, all resources from this project are gone. There is nothing left to accrue charges.
Nice Work!
Nice Work!
You just built a multi-subnet routed network from scratch and proved you can control exactly where packets travel. That is the core skill behind every firewall deployment, network segmentation strategy, and cloud security architecture.
Here's what you accomplished:
- Built a 3-subnet Azure VNet and overrode default routing with User Defined Routes that force traffic through a specific next hop instead of taking the direct path.
- Configured a Linux VM as a Network Virtual Appliance by enabling IP forwarding at both the Azure NIC layer and the Linux kernel layer.
- Diagnosed and fixed routing failures using effective routes, traceroute, and ping to verify packet paths through the NVA.
- Secret Mission: Implemented bidirectional forced routing with a second route table and verified end-to-end HTTP connectivity through the NVA.
Ready to quiz yourself?