Revisited: Working with Hyper-V NAT switches

Foreword

This is an update to a similar post from back in October 2024.

I’ve been going through and cleaning up a lot of my setup scripts and the like in pursuit of SCIENCE easier to use and quickly reproducible testing environments.

Occasionally, I want to configure NAT switches for small-scale, self-contained environments. I didn’t have anything that made this process nice, so I decided to fix something up.

Of course, before I knew it I’d wound up redoing the original post linked above - so I’d might as well post it!

Intro

There’s a type of vSwitch that the Hyper-V MMC does not let you create - the NAT switch.

Why not? Well, it’s not a VMSwitch type, per say - it would be more accurate to describe this as some configuration on the host to forward packets from a subnet on an interface that happens to be attached to an internal vSwitch.

You’ve probably seen this before if you’ve ever installed Hyper-V on a Windows 10 or 11 machine - the Default Switch is effectively a NAT switch with some Internet Connection Sharing stuff and address overlap detection going on behind the scenes.

Anyway, having NAT switches is super useful if you’re running stuff on your laptop or in an environment you do not entirely control:

Create the switch

Basic config requires an Internal VMSwitch, assigning an IP address to your host machine on said VMSwitch, and then creating a NetNat object on that network.

Here’s an example config:

# parameters
$IPAddress = "192.0.2.1"
$PrefixLength = 24
$SwitchName = "vlab0-natswitch"

# create the switch

$Switch = New-VMSwitch `
	-SwitchName $SwitchName `
	-SwitchType Internal

# configure addressing for the gateway (hypervisor OS)

$NetAdapter = Get-NetAdapter |
	Where Name -eq "vEthernet ($($Switch.Name))"

New-NetIPAddress `
	-InterfaceIndex $NetAdapter.ifIndex `
	-IPAddress $IPAddress `
	-PrefixLength $PrefixLength

# create a new NAT object

New-NetNat `
	-Name $SwitchName `
	-InternalIPInterfaceAddressPrefix "$($IPAddress)/$($PrefixLength)"

Let’s package this up into something a little nicer to use.

This can be found on GitHub.

<#
.SYNOPSIS
Create an internal VMSwitch and configure host to do NAT between it and the outside world.

Optionally, configure forwarding between this NAT network and the WSL VMSwitch.

.PARAMETER IPv4Address
String: The IPv4 address that will be configured on the host's vEthernet interface for the new VMSwitch. Default: '192.0.2.1'.

.PARAMETER PrefixLength
String: The prefix length that will be configured on the host's vEthernet interface for the new VMSwitch, e.g. 16. Default: '24'.

.PARAMETER Name
String: The name of the new VMSwitch. Default: 'Internal NAT'.

.PARAMETER EnableForwarding
Boolean: Enable forwarding on the host's vEthernet interface for this network. Default: $true.
This DOES NOT affect forwarding to things outside the host, just between VMSwitches.

.PARAMETER EnableWSLForwarding
Boolean: Enable forwarding on the WSL interface. Convenience option to allow Ansible or SSH from WSL. Default: $false.

.PARAMETER NetNatName
String: The name of the new NetNat object. Default: same as VMSwitch name ($Name).

.PARAMETER Force
Switch: Overwrite existing VMSwitch and NetNat objects, if applicable.

.EXAMPLE
New-NatVMSwitch `
    -IPv4Address '10.128.252.254' `
    -PrefixLength 24 `
    -Name 'Example Network' `
    -EnableForwarding $true `
    -RouteWSL $true `
    -Force
#>
Function New-NatVMSwitch {
	param(
		[string]$IPv4Address = '192.0.2.1',
		[short]$PrefixLength = 24,
		[string]$Name = 'Internal NAT',
        [bool]$EnableForwarding = $true,
        [bool]$EnableWSLForwarding = $false,
        [string]$NetNatName = $Name,
        [switch]$Force
	)

    try {

        # verify that a VMSwitch does not exist, and/or remove the existing one if -Force is set

        $ExistingVMSwitch = Get-VMSwitch `
            -Name $Name `
            -ErrorAction SilentlyContinue

        if ($ExistingVMSwitch) {

            if ($Force) {

                Write-Information `
                "Attempting to remove existing VMSwitch $($Name) because -Force is set."

                Remove-VMSwitch `
                    -Name $Name `
                    -Force `
                    -Confirm:$false `
                    -ErrorAction Stop
                
            } else {
                
                throw `
                    "A VMSwitch with name '$($Name)' already exists."

            }

        }

        # verify that a NetNat object does not exist, and/or remove the existing one if -Force is set

        $ExistingNetNat = Get-NetNat `
            -Name $NetNatName `
            -ErrorAction SilentlyContinue

        if ($ExistingNetNat) {

            if ($Force) {

                Write-Information `
                    "Attempting to remove existing NetNat object $($NetNatName) because -Force is set."

                Remove-NetNat `
                    -Name $NetNatName `
                    -Confirm:$false `
                    -ErrorAction Stop

            } else {

                throw `
                    "A NetNat object with the name '$($Name)' already exists."
                
            }
            
        }

        # create an internal VMSwitch with requested name

        try {
            
            $Switch = New-VMSwitch `
                -SwitchName $Name `
                -SwitchType Internal `
                -ErrorAction Stop

        } catch {

            throw `
                "Failed to create VMSwitch '$($Name)': $($_)"

        }

        # find the newly created vEthernet adapter (the host's network adapter on the new VMSwitch)

        $NetAdapter = Get-NetAdapter |
        Where-Object Name -eq "vEthernet ($($Switch.Name))"

        if (-not $NetAdapter) {
            
            throw `
                "The virtual network adapter 'vEthernet ($($Name))' could not be found."

        }

        # assign the requested IPv4 address and prefix length to the switch's vEthernet adapter

        try {
            
            New-NetIPAddress `
                -InterfaceIndex $NetAdapter.ifIndex `
                -IPAddress $IPv4Address `
                -PrefixLength $PrefixLength `
                -ErrorAction Stop

        } catch {

            throw `
                "Failed to create NetIPAddress on interface $($NetAdapter.ifIndex): $($_)"

        }

        # create a new NetNat object to configure the host to forward traffic from a network

        try {

            New-NetNat `
                -Name $NetNatName `
                -InternalIPInterfaceAddressPrefix "$($IPv4Address)/$($PrefixLength)" `
                -ErrorAction Stop
            
        } catch {
            
            throw `
                "Failed to create NetNat object: $($_)"

        }

        # Set the Forwarding property of a NetIPInterface to Enabled by alias.

        function Enable-Forwarding {
            param(
                [string]$InterfaceAlias
            )

            $Interface = Get-NetIPInterface `
                -InterfaceAlias $InterfaceAlias

            if (-not $Interface) {

                throw `
                    "The ifAlias $($InterfaceAlias)'s associated interface could not be found. Failed to enable forwarding on interface."

            }

            Set-NetIPInterface `
                -InputObject $Interface `
                -Forwarding Enabled
            
        }

        # if enabling forwarding (on the new VMSwitch, to other VMSwitches) is desired, do so using the above wrapper

        if ($EnableForwarding) {

            Enable-Forwarding `
                -InterfaceAlias "vEthernet ($($Name))"

        }

        # if enabling forwarding (WSL to other VMSwitches) is desired, do so using the above wrapper

        if ($EnableWSLForwarding) {

            Enable-Forwarding `
                -InterfaceAlias 'vEthernet (WSL (Hyper-V firewall))'

        }

    } catch {

        Write-Error `
            "Script terminated with error: $($_)"

    }

}

This script will create a VMSwitch and NetNat object, and can optionally reconfigure the WSL VMSwitch.

PS C:\Users\liam> get-netnat

Name                             : Internal NAT
ExternalIPInterfaceAddressPrefix :
InternalIPInterfaceAddressPrefix : 192.0.2.1/24
IcmpQueryTimeout                 : 30
TcpEstablishedConnectionTimeout  : 1800
TcpTransientConnectionTimeout    : 120
TcpFilteringBehavior             : AddressDependentFiltering
UdpFilteringBehavior             : AddressDependentFiltering
UdpIdleSessionTimeout            : 120
UdpInboundRefresh                : False
Store                            : Local
Active                           : True

PS C:\Users\liam> sudo pwsh
PowerShell 7.5.0
PS C:\Users\liam> get-vmswitch -Name 'Internal NAT'

Name         SwitchType NetAdapterInterfaceDescription
----         ---------- ------------------------------
Internal NAT Internal

PS C:\Users\liam> get-netipinterface | where InterfaceAlias -like *WSL* | select Forwarding

Forwarding
----------
  Enabled
  Enabled

To clean up after it:

# delete the NetNat object
PS C:\Users\liam> get-netnat -name 'Internal NAT' | remove-netnat -confirm:$false

# delete the VMSwitch
PS C:\Users\liam> get-vmswitch -Name 'Internal NAT' | remove-vmswitch -confirm:$false

# unset forwarding on the WSL interface, if you configured it
PS C:\Users\liam> get-netipinterface | where InterfaceAlias -like *WSL* | set-netipinterface -forwarding Disabled