If you're building Azure infrastructure and plan to connect different services together with service endpoints and/or private link, then you might not always know if the setup if done correctly. It would be easier to just test with simple test application that network setup works as expected. This app is just for that!
You can deploy this application using container to e.g. app service or AKS and then invoke it's exposed api to make different network operations.
See example end-to-end scenario Azure Firewall Demo for more details. Webapp for network testing is used in that implementation for testing various firewall rules.
It currently has support for following operations:
| Command | Sub-command | Description |
|---|---|---|
| HTTP | GET | Invokes GET request to the parameter url |
| HTTP | POST | Invokes POST request to the parameter url and passes further command to the target address |
| TCP | N/A | Connects to target host and port according to parameters |
| BLOB | GET | Downloads blob according to parameters defining file, container and storage account |
| BLOB | POST | Uploads blob according to parameters defining file, container and storage account |
| FILE | LIST | List files from filesystem according to parameter defining directory path |
| FILE | READ | Read file from filesystem according to parameter defining file path |
| FILE | WRITE | Write file from filesystem according to parameters defining file path and content |
| REDIS | GET | Gets item from cache according to parameters defining key and redis cache |
| REDIS | SET | Sets item from cache according to parameters defining key and redis cache |
| SQL | QUERY | Executes SQL query according to parameters |
| IPLOOKUP | N/A | Gets IP address of defined in parameter |
| NSLOOKUP | N/A | Get IP address and relevant network related information about address defined in parameter |
| INFO | HOSTNAME | Gets hostname of the container |
| INFO | NETWORK | Gets network interface details such as gateway and DNS servers addresses |
| INFO | ENV | Gets single or all environment variables |
| HEADER | NAME | Gets single or all HTTP headers |
| CONNECTION | IP | Gets remote address IP |
| CONNECTION | N/A | Gets remote connection information |
You can use curl to invoke the api. For example, to invoke HTTP GET request to https://github.com:
curl -X POST --data 'HTTP GET "https://github.com"' https://localhost:44328/api/commandsAlternative, you can use swagger endpoint to invoke the api directly in browser:
https://localhost:44328/swagger/index.html
You simple create plain text payload with single command per line and send it to the exposed api endpoint /api/commands:
REDIS SET value1 mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False
REDIS GET mycache account.redis.cache.windows.net:6380,password=key=,ssl=True,abortConnect=False
Here is example response:
-> Start: HTTP POST http://localhost:5000/api/commands
-> Start: REDIS SET hello2 mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False
SET: mycache=value1
<- End: REDIS SET hello2 mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False
-> Start: REDIS GET mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False
GET: value1
<- End: REDIS GET mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False
<- End: HTTP POST http://localhost:5000/api/commands
Here are few example command payloads:
Invokes GET request to target address http://target/:
HTTP GET http://target/
Invokes POST request to target address AND passes along rest of the commands for further processing:
HTTP POST https://target/api/commands
Note: Both HTTP GET and HTTP POST support sending HTTP Headers as third parameter. Here's example about that:
HTTP POST "https://echo.contoso.com/api/echo" "CustomHeader1=Value1|CustomHeader2=Value2"
Downloads file.csv from container files using connection string:
BLOB GET file.csv files DefaultEndpointsProtocol=https;AccountName=account;AccountKey=key;EndpointSuffix=core.windows.net
Download using managed identity authentication:
BLOB GET file.csv files https://account.core.windows.net/
Upload hello as content to file file.csv at container files using connection string:
BLOB POST hello file.csv files DefaultEndpointsProtocol=https;AccountName=account;AccountKey=key;EndpointSuffix=core.windows.net
Upload using managed identity authentication:
BLOB POST hello file.csv files https://account.core.windows.net/
Gets item called mycache from the cache using connection string:
REDIS GET mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False
Sets value hello to the item called mycache using connection string:.
REDIS SET hello mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False
Executes query using connection string:
SQL QUERY "SELECT TOP (5) * FROM [SalesLT].[Customer]" "Server=tcp:server.database.windows.net,1433;Initial Catalog=db;Persist Security Info=False;User ID=user;Password=password;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;"
Executes query using managed identity authentication:
SQL QUERY "SELECT TOP (5) * FROM [SalesLT].[Customer]" "Server=server.database.windows.net;Database=db;Authentication=ActiveDirectoryMsi;TrustServerCertificate=True;"
Gets ip address of the account.redis.cache.windows.net:
IPLOOKUP account.redis.cache.windows.net
Gets ip address of the account.redis.cache.windows.net using 1.1.1.1 (Cloudflare DNS) as name server:
IPLOOKUP account.redis.cache.windows.net 1.1.1.1
Gets ip address and relevant network related information of the account.redis.cache.windows.net:
NSLOOKUP account.redis.cache.windows.net
Gets ip address and relevant network related information of the account.redis.cache.windows.net using 168.63.129.16 (Azure DNS) as name server:
NSLOOKUP account.redis.cache.windows.net 168.63.129.16
Gets ip address and relevant network related information of the account.redis.cache.windows.net using 1.1.1.1 (Cloudflare DNS) as name server:
NSLOOKUP account.redis.cache.windows.net 1.1.1.1
Test connectivity to localhost to port 44328:
TCP localhost 44328
Deploy jannemattila/webapp-network-tester container to your app service(s). Also add following application settings to the apps:
WEBSITE_DNS_SERVER=168.63.129.16WEBSITE_VNET_ROUTE_ALL=1
Read more about these settings from the documentation.
After deployment you can test app with following request:
POST https://*yourapp*.azurewebsites.net/api/commands HTTP/1.1Use simple test payload like this:
HTTP GET http://localhost/You should get following reply:
Hello there!Now you are ready to test you configurations!
Let's validate following architecture:
You can use IPLOOKUP command for for fetching target resource IP:
IPLOOKUP account.blob.core.windows.net=> (output abbreviated)
IP: 52.239.139.132You can double check that IP address using AzureDatacenterIPorNo:
# Import or install if not installed earlier
Import-Module AzureDatacenterIPorNo
Get-AzureDatacenterIPOrNo -IP 52.239.139.132
Source Ip IpRange Region
------ -- ------- ------
PublicIPs_20200504 52.239.139.132 IpRange europenorthSo clearly it's Azure public IP address from North Europe region.
Next let's test that application can indeed use storage as intended:
BLOB POST hello file.csv files DefaultEndpointsProtocol=https;AccountName=account;AccountKey=key;EndpointSuffix=core.windows.net
BLOB GET file.csv files DefaultEndpointsProtocol=https;AccountName=account;AccountKey=key;EndpointSuffix=core.windows.net=> (output abbreviated)
Wrote "0x8D86A928E7D78FD"
helloWe have now verified that application does have access to the blob storage as we wanted.
It has created container files and uploaded and downloaded file called file.csv successfully.
Now you should also validate that there is no access from e.g. Azure Portal or via Azure Storage Explorer for making sure that your setup is correctly done.
Let's validate following architecture:
You can use NSLOOKUP command for testing the DNS setup:
NSLOOKUP account.redis.cache.windows.net=> (output abbreviated)
RECORD: account.redis.cache.windows.net. 120 IN CNAME account.privatelink.redis.cache.windows.net.And then try privatelink address:
NSLOOKUP account.privatelink.redis.cache.windows.net=> (output abbreviated)
RECORD: account.privatelink.redis.cache.windows.net. 10 IN A 172.17.2.4Alternative you can try:
IPLOOKUP account.privatelink.redis.cache.windows.net=> (output abbreviated)
IP: 172.17.2.4Above would indicate that connection would be using internal IP address.
You can also use REDIS command for verifying the setup:
REDIS SET hello mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False
REDIS GET mycache account.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False=> (output abbreviated)
SET: mycache=hello
GET: helloIf the IPLOOKUP or NSLOOKUP would give you external IP address (e.g. 40.86.133.156)
then you can try to force it to use Azure DNS in the lookup:
NSLOOKUP account.redis.cache.windows.net 168.63.129.16If that then works it would mean then you should check your application settings.
Here's high level architecture you might want to implement using app service:
Above can be implemented using following architecture:
Or using following architecture:
NOTE: App services in same app service plan integrate into same subnet using
regional VNet integration. This means that you can either 1) Create 2 separate
app service plans or 2) setup filtering in network security group to just allow
required connectivity between services. You can use outbound IPs of front
app service for the filtering rules.
We can analyze our network setup if we deploy the network test tool to both app services.
You can start analyzing with IPLOOKUP command for checking if private IPs are returned for the database:
IPLOOKUP account.database.windows.net=> (output abbreviated)
IP: 172.17.2.5Now let's try to connect to the database directly from front:
SQL QUERY "SELECT TOP (5) * FROM [SalesLT].[Customer]" "Server=tcp:account.database.windows.net,1433;Initial Catalog=db;Persist Security Info=False;User ID=user;Password=password;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"It tries to connect to the database but then after long timeout (~2 mins) it should fail like this (output abbreviated):
Microsoft.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 40 - Could not open a connection to SQL Server)
Let's validate our backend IP address:
IPLOOKUP *yourbackendapp*.azurewebsites.net=> (output abbreviated)
IP: 172.17.5.4Instead of trying direct connection from front, let's pass that same request
to the backend app:
HTTP POST https://*yourbackendapp*.azurewebsites.net/api/commands
SQL QUERY "SELECT TOP (5) * FROM [SalesLT].[Customer]" "Server=tcp:account.database.windows.net,1433;Initial Catalog=db;Persist Security Info=False;User ID=user;Password=password;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"=> (output abbreviated)
CustomerID;NameStyle;Title;FirstName
1;False;Mr.;Orlando
2;False;Mr.;Keith
3;False;Ms.;DonnaThis proves that connectivity is working from the backend app service but
you cannot directly connect from front to the database.
If you want to test App Service multi-container
setup, then create following docker-compose.yml and deploy that to app service:
version: '3.3'
services:
web:
image: jannemattila/webapp-network-tester
environment:
- APP_LAYER=web
restart: always
api:
image: jannemattila/webapp-network-tester
environment:
- APP_LAYER=api
restart: always
db:
image: jannemattila/webapp-network-tester
environment:
- APP_LAYER=db
restart: alwaysAbove will create simple web, api and db
containers for simulating 3-tier application.
If you now want to see the IP address of the api:
IPLOOKUP api=> (output abbreviated)
IP: 172.16.2.3If you want to create chain of calls from web to
api and from api to db, you can use following command
for that:
POST https://*yourapp*.azurewebsites.net/api/commands HTTP/1.1
HTTP POST http://api/api/commands
HTTP POST http://db/api/commands
INFO ENV APP_LAYER=> (output abbreviated)
-> Start: HTTP POST http://api/api/commands
-> Start: HTTP POST http://db/api/commands
-> Start: INFO ENV APP_LAYER
ENV: APP_LAYER: db
<- End: INFO ENV APP_LAYER
<- End: HTTP POST http://db/api/commands
<- End: HTTP POST http://api/api/commandsYou can use e.g., api.ipify.org for testing your outbound IP address:
POST https://*yourapp*.azurewebsites.net/api/commands HTTP/1.1
HTTP GET https://api.ipify.org/=> (output abbreviated)
-> Start: HTTP GET https://api.ipify.org/
20.76.118.228
<- End: HTTP GET https://api.ipify.org/This comes especially handy test, if you plan to deploy NAT Gateway in order to get static IP address for outbound network traffic.
You can use this tool for testing managed identities. Enable managed identity at the Azure Service that you plan to host your app and then test the setup with below commands.
HTTP GET "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2021-02-01&resource=https://management.azure.com/" "Metadata=true"Read more about the acquiring token inside virtual machines.
You can also use this to access Azure Instance Metadata Service.
For App Services you first need to obtain these two environment variable values:
INFO ENV IDENTITY_ENDPOINT
INFO ENV IDENTITY_HEADER=> (output abbreviated)
ENV: IDENTITY_ENDPOINT: http://172.16.0.3:8081/msi/token
ENV: IDENTITY_HEADER: ef98d788-3bb3-4572-bb79-47e2aa1090f9Then you can use them to obtain the token:
HTTP GET http://172.16.0.3:8081/msi/token?api-version=2019-08-01&resource=https://management.azure.com/ "X-IDENTITY-HEADER=ef98d788-3bb3-4572-bb79-47e2aa1090f9"Read more about acquiring token inside app service.
HTTP GET "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2021-02-01&object_id=4f5134e0-ef0f-4402-92de-585290a2284c&resource=https://management.azure.com/" "Metadata=true"Note: There is additional object_id in the request for defining the identity to use. Read this important comment about it as well.
{
"access_token":"eyJ0...EV0w",
"expires_on":"1624011075",
"resource":"https://management.azure.com/",
"token_type":"Bearer",
"client_id":"d3465d27-4178-477e-85d5-c75259776d8d"
}# Build container image
docker build . -f src/WebApp/Dockerfile -t webapp-network-tester:latest
# Build container image with all networking tools
docker build . -f src/WebApp/Full.Dockerfile -t webapp-network-tester:latest-full
# Run container using command
docker run -it --rm -p "2001:8080" webapp-network-tester:latest
# Run container using command all networking tools
docker run -it --rm -p "2001:8080" webapp-network-tester:latest-fullIf you want to publish your image to ACR (instructions):
$acrName = "<your ACR name>"
# Login
az acr login --name $acrName
# Tag image
docker tag webapp-network-tester "$acrName.azurecr.io/webapp-network-tester"
# Push image
docker push "$acrName.azurecr.io/webapp-network-tester"Download the tool:
- Go to Actions or Releases
- Select latest successful run or version
- Download artifact based on your platform
webappnetworktester-windowsfor Windowswebappnetworktester-linuxfor Linuxwebappnetworktester-macosfor macOS
- Extract the artifact
Ready to use! Below commands have optional environment variable ASPNETCORE_URLS for setting the port.
Otherwise it will use default port 8080.
Cmd:
set ASPNETCORE_URLS=http://*:80
webappnetworktester.exePowerShell:
$env:ASPNETCORE_URLS="http://*:80"
.\webappnetworktester.exeBash:
unzip webappnetworktester-linux.zip
chmod +x webappnetworktester
export ASPNETCORE_URLS="http://*:80"
./webappnetworktesterHere's PowerShell example, how you can deploy Azure App Service using image directly from Docker Hub:
$appServiceName="networktester000001"
$appServicePlanName="ntPlan"
$resourceGroup="network-tester-rg"
$location="westeurope"
$image="jannemattila/webapp-network-tester"
# Login to Azure
az login
# List subscriptions
az account list -o table
# *Explicitly* select your working context
az account set --subscription <SubscriptionName>
# Show current context
az account show -o table
# Create new resource group
az group create --name $resourceGroup --location $location -o table
# Create App Service Plan
az appservice plan create --name $appServicePlanName --resource-group $resourceGroup --is-linux --number-of-workers 1 --sku P1V2 -o table
# Create App Service
az webapp create --name $appServiceName --plan $appServicePlanName --resource-group $resourceGroup -i $image -o table
# Set port mapping
az webapp config appsettings set -g $resourceGroup -n $appServiceName --settings WEBSITES_PORT=8080
# Wipe out the resources
az group delete --name $resourceGroup -yExcellent article about multi-tier web applications.




