diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7baa614 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +# Normalize line endings on commit for text files. +* text=auto + +# Scripts executed on Linux VMs (via Custom Script Extension / cloud-init) must +# always be checked out with LF endings. CRLF causes the shebang to fail with +# "bad interpreter: No such file or directory". +*.sh text eol=lf +*.azcli text eol=lf +cloud-init.txt text eol=lf + +# Windows scripts keep CRLF. +*.ps1 text eol=crlf + +# Templates and docs. +*.bicep text eol=lf +*.json text eol=lf +*.md text eol=lf +*.yml text eol=lf diff --git a/.github/workflows/validate-templates.yml b/.github/workflows/validate-templates.yml new file mode 100644 index 0000000..cb76261 --- /dev/null +++ b/.github/workflows/validate-templates.yml @@ -0,0 +1,63 @@ +name: Validate templates + +on: + push: + branches: [master] + paths: ['infra/**', 'scripts/**', '.github/workflows/validate-templates.yml'] + pull_request: + paths: ['infra/**', 'scripts/**', '.github/workflows/validate-templates.yml'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + bicep: + name: Build, lint and check ARM output is in sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Bicep CLI + run: | + curl -sLo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64 + chmod +x bicep && sudo mv bicep /usr/local/bin/bicep + bicep --version + + - name: Lint Bicep sources + run: | + for src in infra/bicep/*.bicep; do + echo "Linting $src" + bicep lint "$src" + done + + # The ARM JSON under infra/arm/ is generated, never hand-edited. Rebuild it + # and fail if the result differs from what is committed. + - name: Rebuild ARM templates and verify they match the committed output + run: | + for src in infra/bicep/*.bicep; do + out="infra/arm/$(basename "${src%.bicep}").json" + bicep build "$src" --outfile "$out" + done + if ! git diff --exit-code -- infra/arm; then + echo "::error::infra/arm/*.json is out of sync with infra/bicep/*.bicep. Run 'az bicep build' and commit the result." + exit 1 + fi + + shell: + name: Shell script syntax and line endings + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # CRLF in a script executed by the Custom Script Extension breaks the + # shebang with "bad interpreter: No such file or directory". + - name: Reject CRLF line endings + run: | + if grep -rlU $'\r' scripts/linux labs; then + echo "::error::Files above contain CRLF line endings; they must be LF." + exit 1 + fi + + - name: Check syntax + run: for f in scripts/linux/*.sh; do bash -n "$f"; done diff --git a/README.md b/README.md index b15eaf3..600d0c1 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,305 @@ -# Deploying Azure VM as Router +

Azure VM as Router

-Deploy Azure VM (Linux or Windows) with IP forwarder enabled to be used as Router. All deployments in this document assumes you have already and existing Virtual Network (VNET) and Subnet. +

+ Deploy an Azure VM (Linux or Windows) with IP forwarding enabled, to be used as a router / Network Virtual Appliance (NVA). +

-## Deploy Linux VM as Router (IPv4 and IPv6) + NAT to Internet +

+ Validate templates + Bicep + Ubuntu + Windows Server + License +

-This template deploys a Linux Router (Ubuntu 24.04 LTS by default, 22.04 LTS selectable via the `osVersion` parameter) to an existing Virtual Network (VNET)/Subnet using a Single NIC + IP Forwarding Enabled. The ARM templates (`LinuxRouter.json`, `LinuxRouter-newsubnet.json`) are generated from the Bicep sources (`LinuxRouter.bicep`, `LinuxRouter-newsubnet.bicep`). +--- -[![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2FLinuxRouter.json) -[![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2FLinuxRouter.json) +## Table of contents -### Network security defaults +- [Overview](#overview) +- [Quick start](#quick-start) +- [Repository structure](#repository-structure) +- [Linux router](#linux-router) + - [Choosing a template](#choosing-a-template) + - [Parameters](#linux-parameters) +- [Windows router](#windows-router) + - [Parameters](#windows-parameters) +- [Network security defaults](#network-security-defaults) +- [Using the router](#using-the-router) +- [Deploying from the command line](#deploying-from-the-command-line) +- [Setup scripts](#setup-scripts) +- [Lab deployment scripts](#lab-deployment-scripts) +- [Working with the templates](#working-with-the-templates) +- [Recent improvements](#recent-improvements) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) -The templates deploy a **Standard SKU** public IP (the previously used Basic SKU was retired by Azure in September 2025). Standard public IPs are *secure by default*, which changes the out-of-the-box connectivity compared to older versions of these templates: +--- -- **Inbound traffic from the Internet is blocked** unless a Network Security Group (NSG) explicitly allows it. This means you cannot SSH into the router from the Internet right after deployment. To manage the VM, either connect from inside your network (the `LinuxRouter-newsubnet.json` template deploys an NSG that allows inbound traffic from RFC 1918 private ranges), use Azure Bastion, or add an NSG rule that allows SSH from your trusted IP addresses. -- **Outbound traffic to the Internet is allowed.** NSGs permit outbound traffic by default, and the attached public IP provides an *explicit* outbound method (SNAT), so the setup script can install packages during provisioning. This also keeps the templates working after Azure's retirement of *default outbound access* for new deployments. -- **If you disable the public IP** (`deployPublicIpAddress=false`), make sure the subnet has another explicit outbound method — e.g. a NAT Gateway, an Azure Firewall / NVA route, or Load Balancer outbound rules. On virtual networks without default outbound access (the default for newly created VNets), the VM otherwise has no Internet access and the setup script cannot install its packages. +## Overview -## Deploy Windows VM as Router (IPv4 and IPv6) +These templates build a single-NIC virtual machine with **IP forwarding enabled** on both the Azure NIC and inside the guest OS, so it can route traffic on behalf of other subnets. A Custom Script Extension applies the in-guest configuration at provisioning time. -This template deploys a Windows (Server 2019 Core - Small Disk) Router to an existing Virtual Network (VNET)/Subnet using a Single NIC + IP Forwarding Enabled. +| | Linux router | Windows router | +|---|---|---| +| Operating system | Ubuntu 24.04 LTS (default) or 22.04 LTS | Windows Server 2025 / 2022 / 2019, Server Core, small disk, Gen 2 | +| IPv4 + IPv6 forwarding | Yes | Yes | +| NAT / SNAT to the internet | Yes (`iptables` masquerade, persisted) | No | +| ICMP echo reply enabled | Yes | Yes (Windows Firewall rule enabled by the script) | +| Trusted Launch | No | Yes (Secure Boot + vTPM) | +| Default size | `Standard_B2s` | `Standard_B2s` | -[![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2FWinRouter.json) -[![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2FWinRouter.json) +> [!NOTE] +> The ARM JSON under `infra/arm/` is **generated** from the Bicep sources under `infra/bicep/`. Edit the Bicep, then rebuild — never hand-edit the JSON. CI enforces this. + +--- + +## Quick start + +Pick a template and deploy straight to the portal: + +| Template | Use when | Deploy | Visualize | +|---|---|---|---| +| **Linux — existing subnet** | You already have the VNET **and** the subnet | [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router.json) | [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router.json) | +| **Linux — new subnet** | You have the VNET and want the template to create a dedicated NVA subnet | [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router-newsubnet.json) | [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router-newsubnet.json) | +| **Windows — existing subnet** | You want a Windows Server router | [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Fwindows-router.json) | [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Fwindows-router.json) | + +> [!IMPORTANT] +> Set **`allowSshFromAddressPrefix`** (Linux) or **`allowRdpFromAddressPrefix`** (Windows) to your own public IP, e.g. `203.0.113.4/32`. Standard SKU public IPs block **all** inbound traffic unless an NSG allows it. See [Network security defaults](#network-security-defaults). + +--- + +## Repository structure + +``` +. +├── .github/workflows/ CI: Bicep build + lint, ARM drift check, shell syntax / CRLF check +├── infra/ +│ ├── bicep/ Template sources — edit these +│ └── arm/ Generated ARM JSON — deploy these, never hand-edit +├── scripts/ +│ ├── linux/ Custom Script Extension payloads (.sh) and cloud-init +│ └── windows/ Custom Script Extension payload (.ps1) +├── labs/ +│ ├── *.azcli End-to-end Azure CLI lab builds +│ └── conf/ Large BGP route lists used for scale testing +├── docs/ Supporting notes +└── README.md +``` + +--- + +## Linux router + +Deploys an Ubuntu router with a single NIC and IP forwarding enabled. The setup script enables IPv4 and IPv6 forwarding, disables ICMP redirects, configures `iptables` SNAT (masquerade) to the internet for private-range sources, and persists all of it across reboots with `netfilter-persistent`. + +### Choosing a template + +| | `linux-router.json` | `linux-router-newsubnet.json` | +|---|---|---| +| Requires an existing VNET | Yes | Yes | +| Requires an existing subnet | Yes (`existingSubnet`) | No — creates it (`subnetName`, `subnetPrefix`) | +| NSG placement | On the **NIC**, so an existing subnet NSG is never overwritten | On the **new subnet** it creates | +| NSG created when `allowSshFromAddressPrefix` is empty | No | Yes, allowing RFC 1918 inbound only | + + + +### Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `virtualMachineName` | string | *(required)* | Name of the router VM. | +| `adminUsername` | string | *(required)* | Local admin user name. | +| `adminPassword` | secure string | *(required)* | Local admin password. | +| `existingVirtualNetworkName` | string | *(required)* | Name of the existing VNET. | +| `existingSubnet` | string | *(required)* | Existing subnet name. **`linux-router` only.** | +| `subnetName` | string | `lxnva-subnet` | Subnet to create. **`linux-router-newsubnet` only.** | +| `subnetPrefix` | string | *(required)* | CIDR for the new subnet, can be as small as /29. **`linux-router-newsubnet` only.** | +| `osVersion` | string | `24.04` | Ubuntu LTS version — `24.04` or `22.04`. | +| `virtualMachineSize` | string | `Standard_B2s` | VM size. | +| `osDiskType` | string | `Standard_LRS` | `Premium_LRS`, `StandardSSD_LRS` or `Standard_LRS`. | +| `deployPublicIpAddress` | bool | `true` | Create a Standard SKU static public IP. | +| `allowSshFromAddressPrefix` | string | `''` | Source prefix allowed inbound on TCP 22. Empty means no SSH rule. | +| `scriptUri` | string | resolved from the template's own URL | Setup script to run. | +| `scriptCmd` | string | `sh linuxrouter.sh` | Command used to run the script. | +| `location` | string | resource group location | Azure region. | + +--- + +## Windows router + +Deploys a **Windows Server Core, small disk, Generation 2** router with Trusted Launch (Secure Boot + vTPM) enabled. The setup script enables IPv4 and IPv6 forwarding on all interfaces and enables the inbound ICMPv4/ICMPv6 echo request firewall rules, which Windows blocks by default. + +> [!NOTE] +> The Windows router forwards traffic but does **not** perform NAT. If you need SNAT to the internet, use the Linux router or add Routing and Remote Access / NAT separately. + + + +### Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `virtualMachineName` | string | *(required)* | Name of the router VM. | +| `adminUsername` | string | *(required)* | Local admin user name. | +| `adminPassword` | secure string | *(required)* | Local admin password. | +| `existingVirtualNetworkName` | string | *(required)* | Name of the existing VNET. | +| `existingSubnet` | string | *(required)* | Existing subnet name. | +| `osVersion` | string | `2025` | Windows Server version — `2025`, `2022` or `2019`. | +| `virtualMachineSize` | string | `Standard_B2s` | VM size. | +| `osDiskType` | string | `Standard_LRS` | `Premium_LRS`, `StandardSSD_LRS` or `Standard_LRS`. | +| `deployPublicIpAddress` | bool | `true` | Create a Standard SKU static public IP. | +| `allowRdpFromAddressPrefix` | string | `''` | Source prefix allowed inbound on TCP 3389. Empty means no RDP rule. | +| `scriptUri` | string | resolved from the template's own URL | Setup script to run. | +| `scriptCmd` | string | `powershell.exe -ExecutionPolicy Unrestricted -File winrouter.ps1` | Command used to run the script. | +| `location` | string | resource group location | Azure region. | + +--- + +## Network security defaults + +The templates deploy a **Standard SKU** public IP, because the Basic SKU was retired by Azure in September 2025. Standard public IPs are *secure by default*, which changes the out-of-the-box behaviour compared to older versions of these templates: + +- **Inbound internet traffic is blocked** unless an NSG explicitly allows it. Set `allowSshFromAddressPrefix` / `allowRdpFromAddressPrefix` to a trusted source — `203.0.113.4/32` for a single address, or a CIDR range for an office network. The template then creates an NSG with the matching management rule (priority 200) plus an RFC 1918 allow rule (priority 300) so forwarded traffic keeps flowing under the new default-deny. Leaving the parameter empty keeps the previous behaviour and means you manage the VM from inside your network or through Azure Bastion. Setting it to `Internet` or `*` allows access from anywhere and is **not recommended**. +- **Outbound internet traffic is allowed.** NSGs permit outbound by default, and the attached public IP provides an *explicit* outbound method (SNAT), so the setup script can install packages during provisioning. This also keeps the templates working after Azure's retirement of default outbound access for new deployments. +- **If you set `deployPublicIpAddress=false`**, make sure the subnet has another explicit outbound method — a NAT Gateway, a route through Azure Firewall or another NVA, or Load Balancer outbound rules. Without one, the VM has no internet access on a modern VNET and the setup script cannot install its packages. + +--- + +## Using the router + +Deploying the VM does not by itself send any traffic through it. To route traffic: + +1. Create a **route table** and add a user-defined route, for example `0.0.0.0/0` with next hop type **Virtual appliance** and the router's **private** IP as the next hop address. +2. Associate the route table with the source subnets whose traffic should traverse the router. +3. Make sure the router's NSG allows the traffic you intend to forward — the templates add an RFC 1918 allow rule for exactly this reason. + +> [!TIP] +> Do **not** associate the route table with the router's own subnet using a default route pointing at itself; that creates a routing loop. + +--- + +## Deploying from the command line + +Deploy the generated ARM template directly from GitHub: + +```bash +az group create -n rg-nva -l eastus + +az deployment group create \ + -g rg-nva \ + --template-uri https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/infra/arm/linux-router.json \ + --parameters \ + virtualMachineName=nva1 \ + adminUsername=azureuser \ + adminPassword='' \ + existingVirtualNetworkName=vnet1 \ + existingSubnet=nva-subnet \ + allowSshFromAddressPrefix="$(curl -s ifconfig.me)/32" +``` + +> [!WARNING] +> `scriptUri` defaults to a path resolved **relative to the template's own URL**, so it automatically follows the branch or fork you deploy from. That resolution relies on `deployment().properties.templateLink`, which does **not** exist when you deploy a local file with `--template-file`. In that case pass the script location explicitly: +> +> ```bash +> --parameters scriptUri=https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/scripts/linux/linuxrouter.sh +> ``` + +--- + +## Setup scripts + +Custom Script Extension payloads under `scripts/`. + +| Script | Purpose | +|---|---| +| `linux/linuxrouter.sh` | **Default.** IPv4/IPv6 forwarding, no ICMP redirects, `iptables` SNAT to the internet, persisted with `netfilter-persistent`. | +| `linux/linuxrouterv2.sh` | Same as above, using `/etc/sysctl.d/` drop-ins instead of editing `/etc/sysctl.conf`. | +| `linux/linuxrouteronly.sh` | Minimal — enables forwarding only, no NAT and no packages installed. | +| `linux/linuxrouterbgp.sh` | Router plus **Quagga** BGP, peering with two route server / peer IPs. | +| `linux/linuxrouterbgpnh.sh` | Quagga BGP with an explicit **next-hop** override for advertised routes. | +| `linux/linuxrouterbgpfrr.sh` | Router plus **FRRouting** BGP. | +| `linux/linuxrouterbgpfrr2.sh` | FRRouting variant used for the second NVA in dual-NVA labs. | +| `linux/linuxrouterbgpfrr2nh.sh` | FRRouting second-NVA variant with a next-hop override. | +| `linux/cloud-init.txt` | cloud-init alternative to the Custom Script Extension. | +| `windows/winrouter.ps1` | Enables forwarding on all interfaces and allows inbound ICMP echo. | + +> [!NOTE] +> Every script that installs packages first runs `cloud-init status --wait`. Without it the extension can race cloud-init while it is still switching the VM to the regional Azure apt mirror, which leaves the on-disk package indexes pointing at the superseded mirror and makes installs fail with `Unable to locate package`. + +--- + +## Lab deployment scripts + +End-to-end environment builds under `labs/`, intended to be run interactively line by line. + +| Script | Builds | +|---|---| +| `deploylinuxnva.azcli` | A VNET with a Linux NVA plus spoke/test VMs and UDRs to validate routing through it. | +| `deploylinuxnvabgp.azcli` | A Linux NVA running BGP, peered with an Azure Route Server. | +| `deploylinuxnvabgpnp.azcli` | The BGP lab with a custom next-hop, plus network test tooling on the test VMs. | +| `conf/*-bgproutes.txt` | Pre-generated route lists (999 to 10240 prefixes) for BGP scale testing. | + +--- + +## Working with the templates + +Rebuild the ARM JSON after changing any Bicep file: + +```bash +az bicep build --file infra/bicep/linux-router.bicep --outfile infra/arm/linux-router.json +az bicep build --file infra/bicep/linux-router-newsubnet.bicep --outfile infra/arm/linux-router-newsubnet.json +az bicep build --file infra/bicep/windows-router.bicep --outfile infra/arm/windows-router.json +``` + +CI runs `bicep lint`, rebuilds every template and fails if `infra/arm/` differs from the committed output. It also checks the shell scripts for syntax errors and rejects CRLF line endings, which break the shebang when the Custom Script Extension runs a script on Linux. + +--- + +## Recent improvements + +The templates and scripts were modernised after several Azure platform retirements broke the original versions. + +| Area | What changed | +|---|---| +| **Infrastructure as code** | Templates converted to **Bicep**; the ARM JSON is now generated output, kept in sync by CI. | +| **Operating systems** | Ubuntu 18.04 and the retired `UbuntuLTS` / `ubuntults` CLI aliases replaced with **Ubuntu 24.04 LTS** (default) and 22.04 LTS. Windows moved from Server 2019 to **Server 2025** Core / small disk / Gen 2, with Trusted Launch. | +| **Public IP** | Basic SKU (retired September 2025) replaced with **Standard SKU, static allocation** across all templates and lab scripts. | +| **Network security** | Added `allowSshFromAddressPrefix` / `allowRdpFromAddressPrefix` so the templates can create the NSG that Standard SKU public IPs now require, together with an RFC 1918 rule so forwarded traffic still flows. Lab scripts that previously created no NSG now create one. | +| **Provisioning reliability** | Fixed a latent **cloud-init race** that intermittently failed package installation with `Unable to locate package netfilter-persistent`. All package-installing scripts now wait for cloud-init to finish first. | +| **Repository layout** | Reorganised into `infra/`, `scripts/`, `labs/` and `docs/`, with consistent file naming. | +| **Quality gates** | Added GitHub Actions validation and a `.gitattributes` that pins shell scripts to LF. | +| **Documentation** | Rewrote this README with parameter references, template comparisons, security guidance and coverage of every script in the repository. | + +All templates and the affected lab scripts were verified by deploying them to Azure and confirming NSG placement, inbound reachability, extension success, in-guest forwarding and NAT state, end-to-end egress through the NVA, and persistence across a reboot. + +--- ## Roadmap -- Add VMSS option for both Linux and Windows deployments -- Add Accelerated Networking option +### Add a VMSS option for both Linux and Windows deployments + +Replace the single-VM deployment with a **Virtual Machine Scale Set in Flexible orchestration mode** so the router tier can scale out and survive the loss of an instance. + +- Place the scale set behind an **internal Standard Load Balancer** with an **HA Ports** rule, so all protocols and ports are distributed, and a health probe that removes unhealthy instances from rotation. +- Point user-defined routes at the **load balancer's frontend IP** instead of a single VM's private IP, so the next hop stays valid as instances come and go. +- Spread instances across **availability zones** for zone resilience, and apply the existing setup scripts through the scale set's extension profile so every new instance is configured identically. +- Design consideration: stateful features such as `iptables` SNAT require **flow symmetry**, so return traffic must reach the same instance that handled the outbound flow. The NAT-to-internet scenario therefore needs per-instance outbound addressing or a NAT Gateway on the subnet rather than per-instance masquerade. Pure forwarding and BGP scenarios do not have this constraint. + +### Add an Accelerated Networking option + +Expose an `acceleratedNetworking` parameter that sets `enableAcceleratedNetworking` on the NIC. Accelerated Networking gives the VM **SR-IOV**, bypassing the host virtual switch to deliver substantially lower latency and jitter, far higher packets-per-second, and lower CPU utilisation per gigabit — all of which are the main throughput limits for a software NVA. + +- Requires a **supported VM size**. The current `Standard_B2s` default is a burstable size and does **not** support Accelerated Networking, so enabling it also means moving to a size such as `Standard_D2s_v5` or larger. +- Should ship with clear guidance mapping expected throughput to VM size, since the NIC setting alone does not lift the size's own bandwidth cap. +- Plan to validate the flag against every supported OS image, since enabling it on an unsupported size or image causes the deployment to fail rather than silently degrade. + +--- + +## Contributing + +Issues and pull requests are welcome. When changing a template, edit the Bicep under `infra/bicep/`, rebuild the ARM JSON, and commit both — CI will fail if they drift apart. + +## License + +Released under the [MIT License](LICENSE). diff --git a/WinRouter.json b/WinRouter.json deleted file mode 100644 index d7a7873..0000000 --- a/WinRouter.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "virtualMachineSize": { - "type": "string", - "defaultValue": "Standard_B2s", - "metadata": { - "description": "VM size" - } - }, - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Windows Router Manchine Name" - } - }, - "osDiskType": { - "type": "string", - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "StandardSSD_LRS", - "Premium_LRS" - ], - "metadata": { - "description": "Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)" - } - }, - "adminUsername": { - "type": "string", - "metadata": { - "description": "Admin username" - } - }, - "adminPassword": { - "type": "securestring", - "metadata": { - "description": "Admin password" - } - }, - "existingVirtualNetworkName": { - "type": "string", - "metadata": { - "description": "Existing Virtual Nework Name" - } - }, - "existingSubnet": { - "type": "string", - "metadata": { - "description": "Type Existing Subnet Name" - } - }, - "scriptUri": { - "defaultValue": "[uri(deployment().properties.templateLink.uri, 'WinRouter.ps1')]", - "type": "string", - "metadata": { "description": "Script that will be executed" } - }, - "scriptCmd": { - "defaultValue": "powershell.exe -ExecutionPolicy Unrestricted -File WinRouter.ps1", - "type": "string", - "metadata": { "description": "Command to run the script" } - } - }, - "variables": { - "extensionName":"CustomScript", - "NIC": "[concat(parameters('virtualMachineName'),'-NIC')]", - "publicIPAddressName": "[concat(parameters('virtualMachineName'),'-PublicIP')]", - "subnet1Ref": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('existingVirtualNetworkName'), parameters('existingSubnet'))]", - "location": "[resourceGroup().location]" - }, - "resources": [ - { - "name": "[parameters('virtualMachineName')]", - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2017-03-30", - "location": "[variables('location')]", - "comments": "This is the virtual machine that you're building.", - "dependsOn": [ - "[variables('NIC')]" - ], - "properties": { - "osProfile": { - "computerName": "[parameters('virtualMachineName')]", - "adminUsername": "[parameters('adminUsername')]", - "adminPassword": "[parameters('adminPassword')]" - }, - "hardwareProfile": { - "vmSize": "[parameters('virtualMachineSize')]" - }, - "storageProfile": { - "imageReference": { - "publisher": "MicrosoftWindowsServer", - "offer": "WindowsServer", - "sku": "2019-Datacenter-Core-smalldisk", - "version": "latest" - }, - "osDisk": { - "createOption": "FromImage", - "managedDisk": { - "storageAccountType": "[parameters('osDiskType')]" - } - }, - "dataDisks": [] - }, - "networkProfile": { - "networkInterfaces": [ - { - "properties": { - "primary": true - }, - "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('NIC'))]" - } - ] - } - } - }, - { - "name": "[variables('NIC')]", - "type": "Microsoft.Network/networkInterfaces", - "apiVersion": "2017-06-01", - "location": "[variables('location')]", - "dependsOn": [ - "[variables('publicIpAddressName')]" - ], - "properties": { - "enableIPForwarding": true, - "ipConfigurations": [ - { - "name": "ipconfig1", - "properties": { - "subnet": { - "id": "[variables('subnet1Ref')]" - }, - "privateIPAllocationMethod": "Dynamic", - "publicIpAddress": { - "id": "[resourceId('Microsoft.Network/publicIpAddresses', variables('publicIpAddressName'))]" - } - } - } - ] - } - }, - { - "name": "[variables('publicIpAddressName')]", - "type": "Microsoft.Network/publicIPAddresses", - "apiVersion": "2017-06-01", - "location": "[variables('location')]", - "comments": "Public IP for your Primary NIC", - "properties": { - "publicIPAllocationMethod": "Dynamic" - } - }, - { - "type": "Microsoft.Compute/virtualMachines/extensions", - "name": "[concat(parameters('virtualMachineName'), '/', variables('extensionName'))]", - "apiVersion": "2015-06-15", - "location": "[variables('location')]", - "dependsOn": [ - "[concat('Microsoft.Compute/virtualMachines/', parameters('virtualMachineName'))]" - ], - "properties": { - "publisher": "Microsoft.Compute", - "type": "CustomScriptExtension", - "typeHandlerVersion": "1.10", - "autoUpgradeMinorVersion": true, - "settings": { - "fileUris": [ - "[parameters('scriptUri')]" - ], - "commandToExecute": "[parameters('scriptCmd')]" - } - } - } - ], - "outputs": {} -} \ No newline at end of file diff --git a/notes.txt b/docs/notes.md similarity index 87% rename from notes.txt rename to docs/notes.md index 291ddcd..e8c2f99 100644 --- a/notes.txt +++ b/docs/notes.md @@ -7,6 +7,6 @@ az vm create \ --custom-data cloud-init.txt -az network public-ip create --name $spoke1name-vm-pip --resource-group $rg --location $vhub1location --allocation-method Dynamic --output none +az network public-ip create --name $spoke1name-vm-pip --resource-group $rg --location $vhub1location --allocation-method Static --sku Standard --output none az network nic create --resource-group $rg -n $spoke1name-vm-nic --location $vhub1location --subnet vmsubnet --vnet-name $spoke1name-vnet --public-ip-address $spoke1name-vm-pip --output none az vm create -n $spoke1name-vm -g $rg --image Ubuntu2404 --size Standard_B1s --admin-username $username --admin-password $password --nics $spoke1name-vm-nic --no-wait --location $vhub1location --output none diff --git a/LinuxRouter-newsubnet.json b/infra/arm/linux-router-newsubnet.json similarity index 84% rename from LinuxRouter-newsubnet.json rename to infra/arm/linux-router-newsubnet.json index 6b7cd47..db43b74 100644 --- a/LinuxRouter-newsubnet.json +++ b/infra/arm/linux-router-newsubnet.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "8094453408631886471" + "version": "0.46.1.21595", + "templateHash": "2884941074303245104" } }, "parameters": { @@ -78,7 +78,7 @@ }, "scriptUri": { "type": "string", - "defaultValue": "[uri(deployment().properties.templateLink.uri, 'linuxrouter.sh')]", + "defaultValue": "[uri(deployment().properties.templateLink.uri, '../../scripts/linux/linuxrouter.sh')]", "metadata": { "description": "Script that will be executed" } @@ -103,12 +103,20 @@ "metadata": { "description": "Deploy Public IP Address" } + }, + "allowSshFromAddressPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Source address prefix allowed to reach the VM on TCP 22, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need SSH from the internet. Use Internet to allow any source (not recommended)." + } } }, "variables": { "extensionName": "CustomScript", "nicName": "[format('{0}-NIC', parameters('virtualMachineName'))]", "publicIPAddressName": "[format('{0}-PublicIP', parameters('virtualMachineName'))]", + "sshSecurityRules": "[if(empty(parameters('allowSshFromAddressPrefix')), createArray(), createArray(createObject('name', 'Allow-SSH-Inbound', 'properties', createObject('priority', 200, 'protocol', 'Tcp', 'access', 'Allow', 'direction', 'Inbound', 'sourceAddressPrefix', parameters('allowSshFromAddressPrefix'), 'sourcePortRange', '*', 'destinationAddressPrefix', '*', 'destinationPortRange', '22'))))]", "osVersionDefinitions": { "22.04": { "publisher": "Canonical", @@ -131,25 +139,7 @@ "name": "default-nsg", "location": "[parameters('location')]", "properties": { - "securityRules": [ - { - "name": "Allow-Traffic-RFC-1918", - "properties": { - "priority": 300, - "protocol": "*", - "access": "Allow", - "direction": "Inbound", - "sourceAddressPrefixes": [ - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16" - ], - "sourcePortRange": "*", - "destinationAddressPrefix": "*", - "destinationPortRange": "*" - } - } - ] + "securityRules": "[concat(variables('sshSecurityRules'), createArray(createObject('name', 'Allow-Traffic-RFC-1918', 'properties', createObject('priority', 300, 'protocol', '*', 'access', 'Allow', 'direction', 'Inbound', 'sourceAddressPrefixes', createArray('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'), 'sourcePortRange', '*', 'destinationAddressPrefix', '*', 'destinationPortRange', '*'))))]" } }, { diff --git a/LinuxRouter.json b/infra/arm/linux-router.json similarity index 73% rename from LinuxRouter.json rename to infra/arm/linux-router.json index 12597fd..ee7149f 100644 --- a/LinuxRouter.json +++ b/infra/arm/linux-router.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "17441905376390406829" + "version": "0.46.1.21595", + "templateHash": "8873189144452211355" } }, "parameters": { @@ -71,7 +71,7 @@ }, "scriptUri": { "type": "string", - "defaultValue": "[uri(deployment().properties.templateLink.uri, 'linuxrouter.sh')]", + "defaultValue": "[uri(deployment().properties.templateLink.uri, '../../scripts/linux/linuxrouter.sh')]", "metadata": { "description": "Script that will be executed" } @@ -96,13 +96,22 @@ "metadata": { "description": "Deploy Public IP Address" } + }, + "allowSshFromAddressPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Source address prefix allowed to reach the VM on TCP 22, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need SSH from the internet. Use Internet to allow any source (not recommended)." + } } }, "variables": { "extensionName": "CustomScript", "nicName": "[format('{0}-NIC', parameters('virtualMachineName'))]", + "nsgName": "[format('{0}-NSG', parameters('virtualMachineName'))]", "publicIPAddressName": "[format('{0}-PublicIP', parameters('virtualMachineName'))]", "subnetResourceId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('existingVirtualNetworkName'), parameters('existingSubnet'))]", + "deployNetworkSecurityGroup": "[not(empty(parameters('allowSshFromAddressPrefix')))]", "osVersionDefinitions": { "22.04": { "publisher": "Canonical", @@ -159,6 +168,47 @@ "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" ] }, + { + "condition": "[variables('deployNetworkSecurityGroup')]", + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2024-05-01", + "name": "[variables('nsgName')]", + "location": "[parameters('location')]", + "properties": { + "securityRules": [ + { + "name": "Allow-SSH-Inbound", + "properties": { + "priority": 200, + "protocol": "Tcp", + "access": "Allow", + "direction": "Inbound", + "sourceAddressPrefix": "[parameters('allowSshFromAddressPrefix')]", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "22" + } + }, + { + "name": "Allow-Traffic-RFC-1918", + "properties": { + "priority": 300, + "protocol": "*", + "access": "Allow", + "direction": "Inbound", + "sourceAddressPrefixes": [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ], + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "*" + } + } + ] + } + }, { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2024-05-01", @@ -166,6 +216,7 @@ "location": "[parameters('location')]", "properties": { "enableIPForwarding": true, + "networkSecurityGroup": "[if(variables('deployNetworkSecurityGroup'), createObject('id', resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))), null())]", "ipConfigurations": [ { "name": "ipconfig1", @@ -180,6 +231,7 @@ ] }, "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]", "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]" ] }, diff --git a/infra/arm/windows-router.json b/infra/arm/windows-router.json new file mode 100644 index 0000000..d201878 --- /dev/null +++ b/infra/arm/windows-router.json @@ -0,0 +1,287 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.46.1.21595", + "templateHash": "8395921165082925479" + } + }, + "parameters": { + "virtualMachineSize": { + "type": "string", + "defaultValue": "Standard_B2s", + "metadata": { + "description": "VM size" + } + }, + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Windows Router Machine Name" + } + }, + "osDiskType": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "StandardSSD_LRS", + "Premium_LRS" + ], + "metadata": { + "description": "Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)" + } + }, + "osVersion": { + "type": "string", + "defaultValue": "2025", + "allowedValues": [ + "2019", + "2022", + "2025" + ], + "metadata": { + "description": "Windows Server version. All options are Server Core, small disk, Generation 2 images." + } + }, + "adminUsername": { + "type": "string", + "metadata": { + "description": "Admin username" + } + }, + "adminPassword": { + "type": "securestring", + "metadata": { + "description": "Admin password" + } + }, + "existingVirtualNetworkName": { + "type": "string", + "metadata": { + "description": "Existing Virtual Network Name" + } + }, + "existingSubnet": { + "type": "string", + "metadata": { + "description": "Type Existing Subnet Name" + } + }, + "scriptUri": { + "type": "string", + "defaultValue": "[uri(deployment().properties.templateLink.uri, '../../scripts/windows/winrouter.ps1')]", + "metadata": { + "description": "Script that will be executed" + } + }, + "scriptCmd": { + "type": "string", + "defaultValue": "powershell.exe -ExecutionPolicy Unrestricted -File winrouter.ps1", + "metadata": { + "description": "Command to run the script" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region for all resources." + } + }, + "deployPublicIpAddress": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Deploy Public IP Address" + } + }, + "allowRdpFromAddressPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Source address prefix allowed to reach the VM on TCP 3389, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need RDP from the internet. Use Internet to allow any source (not recommended)." + } + } + }, + "variables": { + "extensionName": "CustomScript", + "nicName": "[format('{0}-NIC', parameters('virtualMachineName'))]", + "nsgName": "[format('{0}-NSG', parameters('virtualMachineName'))]", + "publicIPAddressName": "[format('{0}-PublicIP', parameters('virtualMachineName'))]", + "subnetResourceId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('existingVirtualNetworkName'), parameters('existingSubnet'))]", + "deployNetworkSecurityGroup": "[not(empty(parameters('allowRdpFromAddressPrefix')))]", + "osVersionDefinitions": { + "2019": { + "publisher": "MicrosoftWindowsServer", + "offer": "WindowsServer", + "sku": "2019-datacenter-core-smalldisk-g2", + "version": "latest" + }, + "2022": { + "publisher": "MicrosoftWindowsServer", + "offer": "WindowsServer", + "sku": "2022-datacenter-core-smalldisk-g2", + "version": "latest" + }, + "2025": { + "publisher": "MicrosoftWindowsServer", + "offer": "WindowsServer", + "sku": "2025-datacenter-core-smalldisk-g2", + "version": "latest" + } + } + }, + "resources": [ + { + "condition": "[variables('deployNetworkSecurityGroup')]", + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2024-05-01", + "name": "[variables('nsgName')]", + "location": "[parameters('location')]", + "properties": { + "securityRules": [ + { + "name": "Allow-RDP-Inbound", + "properties": { + "priority": 200, + "protocol": "Tcp", + "access": "Allow", + "direction": "Inbound", + "sourceAddressPrefix": "[parameters('allowRdpFromAddressPrefix')]", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "3389" + } + }, + { + "name": "Allow-Traffic-RFC-1918", + "properties": { + "priority": 300, + "protocol": "*", + "access": "Allow", + "direction": "Inbound", + "sourceAddressPrefixes": [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ], + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "*" + } + } + ] + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-07-01", + "name": "[parameters('virtualMachineName')]", + "location": "[parameters('location')]", + "properties": { + "osProfile": { + "computerName": "[parameters('virtualMachineName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]" + }, + "hardwareProfile": { + "vmSize": "[parameters('virtualMachineSize')]" + }, + "securityProfile": { + "securityType": "TrustedLaunch", + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + } + }, + "storageProfile": { + "imageReference": "[variables('osVersionDefinitions')[parameters('osVersion')]]", + "osDisk": { + "createOption": "FromImage", + "name": "[format('{0}-OSDisk', parameters('virtualMachineName'))]", + "managedDisk": { + "storageAccountType": "[parameters('osDiskType')]" + } + }, + "dataDisks": [] + }, + "networkProfile": { + "networkInterfaces": [ + { + "properties": { + "primary": true + }, + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + ] + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2024-05-01", + "name": "[variables('nicName')]", + "location": "[parameters('location')]", + "properties": { + "enableIPForwarding": true, + "networkSecurityGroup": "[if(variables('deployNetworkSecurityGroup'), createObject('id', resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))), null())]", + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "subnet": { + "id": "[variables('subnetResourceId')]" + }, + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": "[if(parameters('deployPublicIpAddress'), createObject('id', resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))), null())]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]", + "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]" + ] + }, + { + "condition": "[parameters('deployPublicIpAddress')]", + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2024-05-01", + "name": "[variables('publicIPAddressName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "publicIPAllocationMethod": "Static" + } + }, + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-07-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), variables('extensionName'))]", + "location": "[parameters('location')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "fileUris": [ + "[parameters('scriptUri')]" + ], + "commandToExecute": "[parameters('scriptCmd')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/LinuxRouter-newsubnet.bicep b/infra/bicep/linux-router-newsubnet.bicep similarity index 85% rename from LinuxRouter-newsubnet.bicep rename to infra/bicep/linux-router-newsubnet.bicep index 886a7ca..5a4d2ca 100644 --- a/LinuxRouter-newsubnet.bicep +++ b/infra/bicep/linux-router-newsubnet.bicep @@ -33,7 +33,7 @@ param subnetName string = 'lxnva-subnet' param subnetPrefix string @description('Script that will be executed') -param scriptUri string = uri(deployment().properties.templateLink.uri, 'linuxrouter.sh') +param scriptUri string = uri(deployment().properties.templateLink.uri, '../../scripts/linux/linuxrouter.sh') @description('Command to run the script') param scriptCmd string = 'sh linuxrouter.sh' @@ -44,10 +44,29 @@ param location string = resourceGroup().location @description('Deploy Public IP Address') param deployPublicIpAddress bool = true +@description('Source address prefix allowed to reach the VM on TCP 22, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need SSH from the internet. Use Internet to allow any source (not recommended).') +param allowSshFromAddressPrefix string = '' + var extensionName = 'CustomScript' var nicName = '${virtualMachineName}-NIC' var publicIPAddressName = '${virtualMachineName}-PublicIP' +var sshSecurityRules = empty(allowSshFromAddressPrefix) ? [] : [ + { + name: 'Allow-SSH-Inbound' + properties: { + priority: 200 + protocol: 'Tcp' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefix: allowSshFromAddressPrefix + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '22' + } + } +] + var osVersionDefinitions = { '22.04': { publisher: 'Canonical' @@ -67,7 +86,7 @@ resource default_nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { name: 'default-nsg' location: location properties: { - securityRules: [ + securityRules: concat(sshSecurityRules, [ { name: 'Allow-Traffic-RFC-1918' properties: { @@ -85,7 +104,7 @@ resource default_nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { destinationPortRange: '*' } } - ] + ]) } } diff --git a/LinuxRouter.bicep b/infra/bicep/linux-router.bicep similarity index 69% rename from LinuxRouter.bicep rename to infra/bicep/linux-router.bicep index ca04448..52693fa 100644 --- a/LinuxRouter.bicep +++ b/infra/bicep/linux-router.bicep @@ -30,7 +30,7 @@ param existingVirtualNetworkName string param existingSubnet string @description('Script that will be executed') -param scriptUri string = uri(deployment().properties.templateLink.uri, 'linuxrouter.sh') +param scriptUri string = uri(deployment().properties.templateLink.uri, '../../scripts/linux/linuxrouter.sh') @description('Command to run the script') param scriptCmd string = 'sh linuxrouter.sh' @@ -41,10 +41,15 @@ param location string = resourceGroup().location @description('Deploy Public IP Address') param deployPublicIpAddress bool = true +@description('Source address prefix allowed to reach the VM on TCP 22, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need SSH from the internet. Use Internet to allow any source (not recommended).') +param allowSshFromAddressPrefix string = '' + var extensionName = 'CustomScript' var nicName = '${virtualMachineName}-NIC' +var nsgName = '${virtualMachineName}-NSG' var publicIPAddressName = '${virtualMachineName}-PublicIP' var subnetResourceId = resourceId('Microsoft.Network/virtualNetworks/subnets', existingVirtualNetworkName, existingSubnet) +var deployNetworkSecurityGroup = !empty(allowSshFromAddressPrefix) var osVersionDefinitions = { '22.04': { @@ -97,11 +102,53 @@ resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-07-01' = { } } +resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-01' = if (deployNetworkSecurityGroup) { + name: nsgName + location: location + properties: { + securityRules: [ + { + name: 'Allow-SSH-Inbound' + properties: { + priority: 200 + protocol: 'Tcp' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefix: allowSshFromAddressPrefix + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '22' + } + } + { + name: 'Allow-Traffic-RFC-1918' + properties: { + priority: 300 + protocol: '*' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefixes: [ + '10.0.0.0/8' + '172.16.0.0/12' + '192.168.0.0/16' + ] + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '*' + } + } + ] + } +} + resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { name: nicName location: location properties: { enableIPForwarding: true + networkSecurityGroup: deployNetworkSecurityGroup ? { + id: networkSecurityGroup.id + } : null ipConfigurations: [ { name: 'ipconfig1' diff --git a/infra/bicep/windows-router.bicep b/infra/bicep/windows-router.bicep new file mode 100644 index 0000000..648e428 --- /dev/null +++ b/infra/bicep/windows-router.bicep @@ -0,0 +1,209 @@ +@description('VM size') +param virtualMachineSize string = 'Standard_B2s' + +@description('Windows Router Machine Name') +param virtualMachineName string + +@description('Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)') +@allowed([ + 'Standard_LRS' + 'StandardSSD_LRS' + 'Premium_LRS' +]) +param osDiskType string = 'Standard_LRS' + +@description('Windows Server version. All options are Server Core, small disk, Generation 2 images.') +@allowed(['2019', '2022', '2025']) +param osVersion string = '2025' + +@description('Admin username') +param adminUsername string + +@description('Admin password') +@secure() +param adminPassword string + +@description('Existing Virtual Network Name') +param existingVirtualNetworkName string + +@description('Type Existing Subnet Name') +param existingSubnet string + +@description('Script that will be executed') +param scriptUri string = uri(deployment().properties.templateLink.uri, '../../scripts/windows/winrouter.ps1') + +@description('Command to run the script') +param scriptCmd string = 'powershell.exe -ExecutionPolicy Unrestricted -File winrouter.ps1' + +@description('Azure region for all resources.') +param location string = resourceGroup().location + +@description('Deploy Public IP Address') +param deployPublicIpAddress bool = true + +@description('Source address prefix allowed to reach the VM on TCP 3389, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need RDP from the internet. Use Internet to allow any source (not recommended).') +param allowRdpFromAddressPrefix string = '' + +var extensionName = 'CustomScript' +var nicName = '${virtualMachineName}-NIC' +var nsgName = '${virtualMachineName}-NSG' +var publicIPAddressName = '${virtualMachineName}-PublicIP' +var subnetResourceId = resourceId('Microsoft.Network/virtualNetworks/subnets', existingVirtualNetworkName, existingSubnet) +var deployNetworkSecurityGroup = !empty(allowRdpFromAddressPrefix) + +var osVersionDefinitions = { + '2019': { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2019-datacenter-core-smalldisk-g2' + version: 'latest' + } + '2022': { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2022-datacenter-core-smalldisk-g2' + version: 'latest' + } + '2025': { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2025-datacenter-core-smalldisk-g2' + version: 'latest' + } +} + +resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-01' = if (deployNetworkSecurityGroup) { + name: nsgName + location: location + properties: { + securityRules: [ + { + name: 'Allow-RDP-Inbound' + properties: { + priority: 200 + protocol: 'Tcp' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefix: allowRdpFromAddressPrefix + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '3389' + } + } + { + name: 'Allow-Traffic-RFC-1918' + properties: { + priority: 300 + protocol: '*' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefixes: [ + '10.0.0.0/8' + '172.16.0.0/12' + '192.168.0.0/16' + ] + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '*' + } + } + ] + } +} + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-07-01' = { + name: virtualMachineName + location: location + properties: { + osProfile: { + computerName: virtualMachineName + adminUsername: adminUsername + adminPassword: adminPassword + } + hardwareProfile: { + vmSize: virtualMachineSize + } + securityProfile: { + securityType: 'TrustedLaunch' + uefiSettings: { + secureBootEnabled: true + vTpmEnabled: true + } + } + storageProfile: { + imageReference: osVersionDefinitions[osVersion] + osDisk: { + createOption: 'FromImage' + name: '${virtualMachineName}-OSDisk' + managedDisk: { + storageAccountType: osDiskType + } + } + dataDisks: [] + } + networkProfile: { + networkInterfaces: [ + { + properties: { + primary: true + } + id: nic.id + } + ] + } + } +} + +resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { + name: nicName + location: location + properties: { + enableIPForwarding: true + networkSecurityGroup: deployNetworkSecurityGroup ? { + id: networkSecurityGroup.id + } : null + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: subnetResourceId + } + privateIPAllocationMethod: 'Dynamic' + publicIPAddress: deployPublicIpAddress ? { + id: publicIpAddress.id + } : null + } + } + ] + } +} + +resource publicIpAddress 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (deployPublicIpAddress) { + name: publicIPAddressName + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource virtualMachineExtension 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = { + parent: virtualMachine + name: extensionName + location: location + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + scriptUri + ] + commandToExecute: scriptCmd + } + } +} diff --git a/conf/1000-bgproutes.txt b/labs/conf/1000-bgproutes.txt similarity index 100% rename from conf/1000-bgproutes.txt rename to labs/conf/1000-bgproutes.txt diff --git a/conf/1024-bgproutes.txt b/labs/conf/1024-bgproutes.txt similarity index 100% rename from conf/1024-bgproutes.txt rename to labs/conf/1024-bgproutes.txt diff --git a/conf/10240-bgproutes.txt b/labs/conf/10240-bgproutes.txt similarity index 100% rename from conf/10240-bgproutes.txt rename to labs/conf/10240-bgproutes.txt diff --git a/conf/4000-bgproutes.txt b/labs/conf/4000-bgproutes.txt similarity index 100% rename from conf/4000-bgproutes.txt rename to labs/conf/4000-bgproutes.txt diff --git a/conf/4001-bgproutes.txt b/labs/conf/4001-bgproutes.txt similarity index 100% rename from conf/4001-bgproutes.txt rename to labs/conf/4001-bgproutes.txt diff --git a/conf/4096-bgproutes.txt b/labs/conf/4096-bgproutes.txt similarity index 100% rename from conf/4096-bgproutes.txt rename to labs/conf/4096-bgproutes.txt diff --git a/conf/5000-bgproutes.txt b/labs/conf/5000-bgproutes.txt similarity index 100% rename from conf/5000-bgproutes.txt rename to labs/conf/5000-bgproutes.txt diff --git a/conf/6000-bgproutes.txt b/labs/conf/6000-bgproutes.txt similarity index 100% rename from conf/6000-bgproutes.txt rename to labs/conf/6000-bgproutes.txt diff --git a/conf/7000-bgproutes.txt b/labs/conf/7000-bgproutes.txt similarity index 100% rename from conf/7000-bgproutes.txt rename to labs/conf/7000-bgproutes.txt diff --git a/conf/9960-bgproutes.txt b/labs/conf/9960-bgproutes.txt similarity index 100% rename from conf/9960-bgproutes.txt rename to labs/conf/9960-bgproutes.txt diff --git a/conf/999-bgproutes.txt b/labs/conf/999-bgproutes.txt similarity index 100% rename from conf/999-bgproutes.txt rename to labs/conf/999-bgproutes.txt diff --git a/deploylinuxnva.azcli b/labs/deploylinuxnva.azcli similarity index 52% rename from deploylinuxnva.azcli rename to labs/deploylinuxnva.azcli index b32168a..1f280e0 100644 --- a/deploylinuxnva.azcli +++ b/labs/deploylinuxnva.azcli @@ -6,20 +6,26 @@ password=Msft123Msft123 vnetname=vnet1 subnetname=nvasubnet nvaname=linux-nva +mypip=$(curl -s -4 ifconfig.io) #your public IP, used to allow inbound SSH #Resource Group az group create --name $rg --location $location ## Create VNET and NVA az network vnet create --resource-group $rg --name $vnetname --location $location --address-prefixes 10.1.0.0/16 --subnet-name $subnetname --subnet-prefix 10.1.10.0/24 -o none -az network vnet subnet create --address-prefix 10.1.0.0/24 --name nvasubnet --resource-group $rg --vnet-name $vnetname -o none + +# NSG: Standard SKU public IPs are closed to inbound traffic until an NSG allows it. +az network nsg create --resource-group $rg --name default-nsg-$location --location $location -o none +az network nsg rule create -g $rg --nsg-name default-nsg-$location -n 'default-allow-ssh' --direction Inbound --priority 100 --source-address-prefixes $mypip --source-port-ranges '*' --destination-address-prefixes '*' --destination-port-ranges 22 --access Allow --protocol Tcp --description "Allow inbound SSH" --output none +az network nsg rule create -g $rg --nsg-name default-nsg-$location -n 'allow-nva-breakout' --direction Inbound --priority 200 --source-address-prefixes VirtualNetwork --source-port-ranges '*' --destination-address-prefixes '*' --destination-port-ranges "*" --access Allow --protocol "*" --description "Allow NVA single NIC use Internet Breakout" --output none +az network vnet subnet update -g $rg --name $subnetname --vnet-name $vnetname --network-security-group default-nsg-$location -o none # NVA + Config script to enable NAT -az network public-ip create --name $nvaname-pip --resource-group $rg --location $location --allocation-method Dynamic +az network public-ip create --name $nvaname-pip --resource-group $rg --location $location --allocation-method Static --sku Standard az network nic create --name $nvaname-nic --resource-group $rg --subnet $subnetname --vnet $vnetname --public-ip-address $nvaname-pip --ip-forwarding true -o none az vm create --resource-group $rg --location $location --name $nvaname --size Standard_B1s --nics $nvaname-nic --image Ubuntu2404 --admin-username $username --admin-password $password -o none # Enable routing and NAT on Linux NVA: -scripturi="https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/linuxrouter.sh" +scripturi="https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/scripts/linux/linuxrouter.sh" az vm extension set --resource-group $rg --vm-name $nvaname --name customScript --publisher Microsoft.Azure.Extensions \ --protected-settings "{\"fileUris\": [\"$scripturi\"],\"commandToExecute\": \"./linuxrouter.sh\"}" \ --no-wait diff --git a/deploylinuxnvabgp.azcli b/labs/deploylinuxnvabgp.azcli similarity index 71% rename from deploylinuxnvabgp.azcli rename to labs/deploylinuxnvabgp.azcli index bdedb5c..ad8af0b 100644 --- a/deploylinuxnvabgp.azcli +++ b/labs/deploylinuxnvabgp.azcli @@ -6,6 +6,7 @@ password=Msft123Msft123 vnetname=vnet1 subnetname=nvasubnet nvaname=linux-nva +mypip=$(curl -s -4 ifconfig.io) #your public IP, used to allow inbound SSH #Resource Group az group create --name $rg --location $location @@ -13,8 +14,14 @@ az group create --name $rg --location $location ## Create VNET and NVA az network vnet create --resource-group $rg --name $vnetname --location $location --address-prefixes 10.1.0.0/16 --subnet-name $subnetname --subnet-prefix 10.1.10.0/24 -o none +# NSG: Standard SKU public IPs are closed to inbound traffic until an NSG allows it. +az network nsg create --resource-group $rg --name default-nsg-$location --location $location -o none +az network nsg rule create -g $rg --nsg-name default-nsg-$location -n 'default-allow-ssh' --direction Inbound --priority 100 --source-address-prefixes $mypip --source-port-ranges '*' --destination-address-prefixes '*' --destination-port-ranges 22 --access Allow --protocol Tcp --description "Allow inbound SSH" --output none +az network nsg rule create -g $rg --nsg-name default-nsg-$location -n 'allow-nva-breakout' --direction Inbound --priority 200 --source-address-prefixes VirtualNetwork --source-port-ranges '*' --destination-address-prefixes '*' --destination-port-ranges "*" --access Allow --protocol "*" --description "Allow NVA single NIC use Internet Breakout" --output none +az network vnet subnet update -g $rg --name $subnetname --vnet-name $vnetname --network-security-group default-nsg-$location -o none + # Enable routing, NAT and BGP on Linux NVA: -az network public-ip create --name $nvaname-pip --resource-group $rg --location $location --allocation-method Dynamic --output none +az network public-ip create --name $nvaname-pip --resource-group $rg --location $location --allocation-method Static --sku Standard --output none az network nic create --name $nvaname-nic --resource-group $rg --subnet $subnetname --vnet $vnetname --public-ip-address $nvaname-pip --ip-forwarding true -o none az vm create --resource-group $rg --location $location --name $nvaname --size Standard_B1s --nics $nvaname-nic --image Ubuntu2404 --admin-username $username --admin-password $password -o none @@ -36,7 +43,7 @@ routeserver_IP1=$(az network routeserver list --resource-group $rg --query '{IPs routeserver_IP2=$(az network routeserver list --resource-group $rg --query '{IPs:[0].virtualRouterIps[1]}' -o tsv) # Enable routing and NAT on Linux NVA: -scripturi="https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/linuxrouterbgp.sh" +scripturi="https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/scripts/linux/linuxrouterbgp.sh" az vm extension set --resource-group $rg --vm-name $nvaname --name customScript --publisher Microsoft.Azure.Extensions \ --protected-settings "{\"fileUris\": [\"$scripturi\"],\"commandToExecute\": \"./linuxrouterbgp.sh $asn_quagga $bgp_routerId $bgp_network1 $routeserver_IP1 $routeserver_IP2\"}" \ --no-wait diff --git a/deploylinuxnvabgpnp.azcli b/labs/deploylinuxnvabgpnp.azcli similarity index 94% rename from deploylinuxnvabgpnp.azcli rename to labs/deploylinuxnvabgpnp.azcli index 6491ab8..a5ad917 100644 --- a/deploylinuxnvabgpnp.azcli +++ b/labs/deploylinuxnvabgpnp.azcli @@ -21,7 +21,7 @@ az network vnet create --resource-group $rg --name $vnetname --location $locatio az network vnet subnet create --address-prefix "10.1.10.0/24" --name $nvasubnetname --resource-group $rg --vnet-name $vnetname --output none # Create Subnet1 VM: -az vm create -n $vnetname-vm1 -g $rg --image ubuntults --public-ip-sku Standard --size $vmsize -l $location --subnet subnet1 --vnet-name $vnetname --admin-username $username --admin-password $password --nsg "" --no-wait +az vm create -n $vnetname-vm1 -g $rg --image Ubuntu2404 --public-ip-sku Standard --size $vmsize -l $location --subnet subnet1 --vnet-name $vnetname --admin-username $username --admin-password $password --nsg "" --no-wait # NSG: az network nsg create --resource-group $rg --name default-nsg-$location --location $location -o none @@ -68,7 +68,7 @@ nvanames=$(i=1;while [ $i -le $instances ];do echo $vnetname-$nvaname$i; ((i++)) for nvaname in $nvanames do # Enable routing, NAT and BGP on Linux NVA: - az vm create -n $nvaname -g $rg --image ubuntults --public-ip-sku Standard --size $vmsize -l $location --subnet $nvasubnetname --vnet-name $vnetname --admin-username $username --admin-password $password --nsg "" --output none + az vm create -n $nvaname -g $rg --image Ubuntu2404 --public-ip-sku Standard --size $vmsize -l $location --subnet $nvasubnetname --vnet-name $vnetname --admin-username $username --admin-password $password --nsg "" --output none #NVA BGP config variables (do not change) bgp_routerId=$(az network nic show --name "$nvaname"VMNic --resource-group $rg --query ipConfigurations[0].privateIpAddress -o tsv) @@ -77,7 +77,7 @@ do # Enable routing and NAT on Linux NVA: # Enable routing, NAT and BGP on Linux NVA: - scripturi="https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/linuxrouterbgpnh.sh" + scripturi="https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/scripts/linux/linuxrouterbgpnh.sh" az vm extension set --resource-group $rg --vm-name $nvaname --name customScript --publisher Microsoft.Azure.Extensions \ --protected-settings "{\"fileUris\": [\"$scripturi\"],\"commandToExecute\": \"./linuxrouterbgpnh.sh $asn_quagga $bgp_routerId $bgp_network1 $routeserver_IP1 $routeserver_IP2 $nexthopip\"}" \ --no-wait diff --git a/cloud-init.txt b/scripts/linux/cloud-init.txt similarity index 100% rename from cloud-init.txt rename to scripts/linux/cloud-init.txt diff --git a/linuxrouter.sh b/scripts/linux/linuxrouter.sh similarity index 82% rename from linuxrouter.sh rename to scripts/linux/linuxrouter.sh index f68afa8..0881819 100644 --- a/linuxrouter.sh +++ b/scripts/linux/linuxrouter.sh @@ -14,6 +14,12 @@ sed -i "/net.ipv6.conf.all.accept_redirects = 0/ s/# *//" /etc/sysctl.conf # The script runs as root (Custom Script Extension), so no sudo is needed. export DEBIAN_FRONTEND=noninteractive +# The Custom Script Extension can start before cloud-init has finished setting +# up the apt sources. Installing then fails with "Unable to locate package" +# because the package indexes on disk belong to the superseded mirror. +echo "Waiting for cloud-init to complete" +cloud-init status --wait >/dev/null 2>&1 || true + echo "Updating repositories" apt-get update -y --fix-missing diff --git a/linuxrouterbgp.sh b/scripts/linux/linuxrouterbgp.sh similarity index 95% rename from linuxrouterbgp.sh rename to scripts/linux/linuxrouterbgp.sh index 0465692..596f61f 100644 --- a/linuxrouterbgp.sh +++ b/scripts/linux/linuxrouterbgp.sh @@ -18,6 +18,10 @@ bgp_network1=$3 routeserver_IP1=$4 routeserver_IP2=$5 +# Wait for cloud-init to finish so apt does not run against superseded +# package indexes, which fails with "Unable to locate package". +cloud-init status --wait >/dev/null 2>&1 || true + sudo apt-get -y update ## Install the Quagga routing daemon diff --git a/scripts/linuxrouterbgpfrr.sh b/scripts/linux/linuxrouterbgpfrr.sh similarity index 92% rename from scripts/linuxrouterbgpfrr.sh rename to scripts/linux/linuxrouterbgpfrr.sh index ccaed29..fd0352b 100644 --- a/scripts/linuxrouterbgpfrr.sh +++ b/scripts/linux/linuxrouterbgpfrr.sh @@ -18,6 +18,10 @@ curl -s https://deb.frrouting.org/frr/keys.asc | sudo apt-key add - FRRVER="frr-stable" echo deb https://deb.frrouting.org/frr $(lsb_release -s -c) $FRRVER | sudo tee -a /etc/apt/sources.list.d/frr.list +# Wait for cloud-init to finish so apt does not run against superseded +# package indexes, which fails with "Unable to locate package". +cloud-init status --wait >/dev/null 2>&1 || true + apt-get -y update apt-get -y install frr frr-pythontools diff --git a/scripts/linuxrouterbgpfrr2.sh b/scripts/linux/linuxrouterbgpfrr2.sh similarity index 92% rename from scripts/linuxrouterbgpfrr2.sh rename to scripts/linux/linuxrouterbgpfrr2.sh index d2d59c1..3aa1fa9 100644 --- a/scripts/linuxrouterbgpfrr2.sh +++ b/scripts/linux/linuxrouterbgpfrr2.sh @@ -19,6 +19,10 @@ curl -s https://deb.frrouting.org/frr/keys.asc | sudo apt-key add - FRRVER="frr-stable" echo deb https://deb.frrouting.org/frr $(lsb_release -s -c) $FRRVER | sudo tee -a /etc/apt/sources.list.d/frr.list +# Wait for cloud-init to finish so apt does not run against superseded +# package indexes, which fails with "Unable to locate package". +cloud-init status --wait >/dev/null 2>&1 || true + apt-get -y update apt-get -y install frr frr-pythontools diff --git a/scripts/linuxrouterbgpfrr2nh.sh b/scripts/linux/linuxrouterbgpfrr2nh.sh similarity index 93% rename from scripts/linuxrouterbgpfrr2nh.sh rename to scripts/linux/linuxrouterbgpfrr2nh.sh index 8d6a4c1..5331cd5 100644 --- a/scripts/linuxrouterbgpfrr2nh.sh +++ b/scripts/linux/linuxrouterbgpfrr2nh.sh @@ -20,6 +20,10 @@ curl -s https://deb.frrouting.org/frr/keys.asc | sudo apt-key add - FRRVER="frr-stable" echo deb https://deb.frrouting.org/frr $(lsb_release -s -c) $FRRVER | sudo tee -a /etc/apt/sources.list.d/frr.list +# Wait for cloud-init to finish so apt does not run against superseded +# package indexes, which fails with "Unable to locate package". +cloud-init status --wait >/dev/null 2>&1 || true + apt-get -y update apt-get -y install frr frr-pythontools diff --git a/linuxrouterbgpnh.sh b/scripts/linux/linuxrouterbgpnh.sh similarity index 95% rename from linuxrouterbgpnh.sh rename to scripts/linux/linuxrouterbgpnh.sh index efaf6cf..f7eb06f 100644 --- a/linuxrouterbgpnh.sh +++ b/scripts/linux/linuxrouterbgpnh.sh @@ -13,6 +13,10 @@ routeserver_IP1=$4 routeserver_IP2=$5 nexthopip=$6 +# Wait for cloud-init to finish so apt does not run against superseded +# package indexes, which fails with "Unable to locate package". +cloud-init status --wait >/dev/null 2>&1 || true + sudo apt-get -y update ## Install the Quagga routing daemon diff --git a/linuxrouteronly.sh b/scripts/linux/linuxrouteronly.sh similarity index 100% rename from linuxrouteronly.sh rename to scripts/linux/linuxrouteronly.sh diff --git a/linuxrouterv2.sh b/scripts/linux/linuxrouterv2.sh similarity index 93% rename from linuxrouterv2.sh rename to scripts/linux/linuxrouterv2.sh index 16f3bca..7b0af49 100644 --- a/linuxrouterv2.sh +++ b/scripts/linux/linuxrouterv2.sh @@ -28,6 +28,10 @@ sysctl --system >/dev/null # --- Packages (non-interactive) --- export DEBIAN_FRONTEND=noninteractive +# Wait for cloud-init to finish so apt does not run against superseded +# package indexes, which fails with "Unable to locate package". +cloud-init status --wait >/dev/null 2>&1 || true + apt-get update -y --fix-missing # Preseed iptables-persistent to disable autosave prompts echo "iptables-persistent iptables-persistent/autosave_v4 boolean false" | debconf-set-selections diff --git a/WinRouter.ps1 b/scripts/windows/winrouter.ps1 similarity index 100% rename from WinRouter.ps1 rename to scripts/windows/winrouter.ps1