Lability tutorial

A tutorial for working with Lability

View project on GitHub

Chapter 6: NAT network configuration

Build a simple NAT network, with one NAT gateway server on the public and private networks, and one NAT’ed server on the private network that accesses the Internet through the NAT gateway.

Last tested: NEVER

On this page

Setting multiple networks on the NAT gateway

The NAT gateway performs routing and NAT services between the NATNET-CORP network and the public network.

RFC 1918 address ranges

When defining private networks, you should always follow the best practice of using RFC 1918 IP address ranges. For IPv4, these ranges are:

  • 192.168.0.0/16
  • 172.16.0.0/12
  • 10.0.0.0/8

Of course, you are free to divide these up into sub networks. In this chapter, we use only the 10.0.0.0/24 network, and leave the rest of the 10.0.0.0/8 network undefined.

Selecting a network range for use on NATNET-CORP

The only important concern for selecting which private IP address range to use in any scenario (including building a lab network in Hyper-V) is to ensure that it will not need to communicate with any other network that shares the same range.

For almost all cases, this means you should use a different network range than the one on your host’s local network. For instance, if your Hyper-V host is attached to a WiFi network using 192.168.0.0/24, then it will have an IP address in the range 192.168.0.0 to 192.168.0.255. In this case, you can use any range in 172.16.0.0/12 or 10.0.0.0/8, or even another part of the 192.168.0.0/16 range that is not defined on your local network, like 192.168.1.0/24 or 192.168.44.0/24.

It’s also worth noting that if you wish to connect your lab VMs to an external VPN for some reason, you will also need to ensure that the network range of the VPN does not overlap with your private network range.

Double NAT

Your host is almost certainly behind a NAT network already, which means that any machine on the private NATNET-CORP network will be double-NAT’ed. This makes protocols that rely on dynamic ports, such as FTP, hard to use behind the double NAT, but for a lab this is probably OK.

This is an example network diagram that includes the host’s local network using 192.168.0.0/24.

network diagram

Multiple network switches for Lability VMs

You can connect a Lability VM to multiple switches by using a Powershell array for the value of Lability_SwitchName. In our config here, we do so with a snippet in our configuration data like so:

@{
    AllNodes = @(
        @{
            NodeName                    = 'NATNET-EDGE1';
            Lability_SwitchName         = @('Wifi-HyperV-VSwitch', 'NATNET-CORP')
        }
    )
}

For each switch in the array, as stated previously, if there is no existing Hyper-V switch with that name, Lability will look in the NonNodeData for a switch configuration that can set what type of switch it is. If none is found, Lability will create an Internal Hyper-V switch.

In the configuration data we use for this example (see below), we define the NATNET-CORP network to be an internal Hyper-V network. This means that it’s only accessible to virtual machines which have an adapter on the network; our host machine has no direct access to the network.

Furthermore, for each switch in the array, Lability will create a separate virtual network adapter in the node; one switch results in one network adapter, two switches in two adapters, etc. By default, Windows names these adapters Ethernet, Ethernet 2, Ethernet 3, etc.

For more information about Hyper-V switch types, see Hyper-V switch types

Hyper-V network order is not predictable

However, unfortunately Hyper-V network adapters are not guaranteed to come up in any given order, which means that in your configuration file, the Wifi-HyperV-VSwitch network could come up attached to the adapter named Ethernet and the NATNET-CORP network could come up attached to the adapter named Ethernet 2, or vice versa, and that this can change every time you run Start-Lab. This causes problems, because it doesn’t allow you to assume that either Ethernet or Ethernet 2 is either network in particular in your configuration document.

We work around this by assigning each adapter a certain MAC address, then using that MAC address in the configuration document to rename the network adapter after the network (so that the adapter on the NATNET-CORP network is renamed to NATNET-CORP), and then finally using those new adapter names when we assign IP addresses or do any other network configuration.

That looks like this in our configuration data:

@{
    AllNodes = @(
        @{
            NodeName                    = 'NATNET-EDGE1';
            Lability_MACAddress         = @('00-15-5d-cf-01-01', '00-15-5d-cf-01-02')
            Lability_SwitchName         = @('Wifi-HyperV-VSwitch', 'NATNET-CORP')
            InterfaceAlias              = @('Public', 'NATNET-CORP')
        }
    )
}

And it looks like this in the configuration script:

node $AllNodes.Where( {$_.Role -in 'EDGE'}).NodeName {

    xNetAdapterName "RenamePublicAdapter" {
        NewName    = $node.InterfaceAlias[0];
        MacAddress = $node.Lability_MACAddress[0];
    }

    # Do not specify an xIPAddress block to set the IP address for the public adapter;
    # this way, it gets an IP address via DHCP

    xNetAdapterName "RenameCorpnetAdapter" {
        NewName    = $node.InterfaceAlias[1];
        MacAddress = $node.Lability_MACAddress[1];
    }

    xIPAddress 'CorpnetIPAddress' {
        IPAddress      = $node.CorpnetIPAddress;
        InterfaceAlias = $node.InterfaceAlias[1];
        AddressFamily  = $node.AddressFamily;
        DependsOn      = '[xNetAdapterName]RenameCorpnetAdapter';
    }
}

This technique was found in the Lability examples:

and mentioned in a Lability issue as a solution for our problem:

Special considerations when assigning MAC addresses

There are two final items to keep in mind when manually assigning MAC addresses to Hyper-V NICs.

  1. Hyper-V has a dedicated MAC address range of 00-15-5d-00-00-00 thru 00-15-5d-ff-ff-ff. You should make sure your MAC address falls within this range.

  2. Ensure no duplicate MAC addresses exist on your public network.

    When assigning a MAC address to an interface, it is important to ensure that it is not a duplicate of any other MAC address on the local network. On physical network interfaces, this assurance comes from the manufacturer, who is assigned a dedicated range of addresses and who commits to assigning a unique MAC address to each NIC it sells. Microsoft has also been assigned a range for Hyper-V NICs, and by default it generates a new NIC in that range for all the virtual NICs owned by each VM.

    However, when we assign MAC addresses ourselves in this chapter, it becomes our role to ensure that no NIC on the local network is assigned the same MAC.

Remoting to machines behind the NAT gateway

We can use PS Remoting to issue commands to VMs behind the NAT gateway, but it is a bit tricky and inconvenient. See Powershell Remoting if you wish to do this.

Lab exercises and files

  1. Deploy the lab with Deploy-NATNET.ps1

  2. Log on to both the gateway machine (EDGE1) and the machine behind the gateway (CLIENT1).

    Prove that each can talk to the Internet by pinging an address or visiting a website.

    Compare the following from each VM:

    • The list of network interfaces from Get-NetIpAddress
    • The routing table from Get-NetRoute
    • The traceroute to a host like CloudFlare’s 1.1.1.1 DNS servers from Test-NetConnection -ComputerName 1.1.1.1 -TraceRoute
  3. Use Powershell Remoting to run commands against the CLIENT1 machine from your lab host

  4. Add a server machine behind the gateway.

    Add a Windows 2016 VM as SERVER1, give it a different role such as SERVER, modify ConfigurationData.NATNET.ps1 to include the xSmbShare DSC resource, and modify Configure-NATNET.ps1 to configure the fileshare. (See documentation for xSmbShare on GitHub.)

    Redeploy the lab with this change, log on to the CLIENT1 machine, and connect to the SMB share you defined.

ConfigurationData.NATNET.psd1

@{
    AllNodes = @(
        @{
            NodeName                    = '*';
            InterfaceAlias              = 'Ethernet';
            AddressFamily               = 'IPv4';
            DnsServerAddress            = '1.1.1.1';
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser        = $true; # Removes 'It is not recommended to use domain credential for node X' messages
            Lability_ProcessorCount     = 1;
            Lability_StartupMemory      = 2GB;
            Lability_Timezone           = "Central Standard Time";
        }
        @{
            NodeName                    = 'NATNET-EDGE1';
            Role                        = 'EDGE'
            CorpnetIPAddress            = '10.0.0.2/24';
            Lability_Media              = "2016_x64_Standard_EN_Eval";

            # Hyper-V MAC address range '00-15-5d-00-00-00' thru '00-15-5d-ff-ff-ff'.
            # WARNING: BE CAREFUL OF DUPLICATE MAC ADDRESSES IF USING EXTERNAL SWITCHES!
            Lability_MACAddress         = @('00-15-5d-cf-01-01', '00-15-5d-cf-01-02')
            Lability_SwitchName         = @('Wifi-HyperV-VSwitch', 'NATNET-CORP')
            InterfaceAlias              = @('Public', 'NATNET-CORP')
        }
        @{
            NodeName                    = 'NATNET-CLIENT1';
            Role                        = 'CLIENT';
            CorpnetIPAddress            = '10.0.0.1/24';
            Lability_SwitchName         = 'NATNET-CORP';
            Lability_Media              = 'WIN10_x64_Enterprise_EN_Eval';
            Lability_Resource           = @(
                'Firefox'
            )
        }
    )
    NonNodeData = @{
        Lability = @{
            Media = @()
            Network = @(
                @{ Name = 'NATNET-CORP'; Type = 'Private'; }
            )
            DSCResource = @(
                @{ Name = 'xComputerManagement'; RequiredVersion = '4.1.0.0'; }
                @{ Name = 'xNetworking'; RequiredVersion = '5.7.0.0'; }
            )
            Resource = @(
                @{
                    Id = 'Firefox'
                    Filename = 'Firefox-Latest.exe'
                    Uri = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US'
                }
            )
        }
    }
}

Configure.NATNET.ps1

Configuration NatNetwork {
    param (
        [Parameter()] [ValidateNotNull()] [PSCredential] $Credential = (Get-Credential -Credential 'Administrator')
    )
    Import-DscResource -Module PSDesiredStateConfiguration

    Import-DscResource -Module xComputerManagement -ModuleVersion 4.1.0.0
    Import-DscResource -Module xNetworking -ModuleVersion 5.7.0.0

    node $AllNodes.Where( {$true}).NodeName {

        LocalConfigurationManager {
            RebootNodeIfNeeded   = $true;
            AllowModuleOverwrite = $true;
            ConfigurationMode    = 'ApplyOnly';
            CertificateID        = $node.Thumbprint;
        }

        xFirewall 'FPS-ICMP4-ERQ-In' {
            Name        = 'FPS-ICMP4-ERQ-In';
            DisplayName = 'File and Printer Sharing (Echo Request - ICMPv4-In)';
            Description = 'Echo request messages are sent as ping requests to other nodes.';
            Direction   = 'Inbound';
            Action      = 'Allow';
            Enabled     = 'True';
            Profile     = 'Any';
        }

        xFirewall 'FPS-ICMP6-ERQ-In' {
            Name        = 'FPS-ICMP6-ERQ-In';
            DisplayName = 'File and Printer Sharing (Echo Request - ICMPv6-In)';
            Description = 'Echo request messages are sent as ping requests to other nodes.';
            Direction   = 'Inbound';
            Action      = 'Allow';
            Enabled     = 'True';
            Profile     = 'Any';
        }

    }

    node $AllNodes.Where( {$_.Role -in 'EDGE'}).NodeName {

        xNetAdapterName "RenamePublicAdapter" {
            NewName    = $node.InterfaceAlias[0];
            MacAddress = $node.Lability_MACAddress[0];
        }
        # Do not specify an IP address for the public adapter so that it gets one via DHCP

        xNetAdapterName "RenameCorpnetAdapter" {
            NewName    = $node.InterfaceAlias[1];
            MacAddress = $node.Lability_MACAddress[1];
        }

        xIPAddress 'CorpnetIPAddress' {
            IPAddress      = $node.CorpnetIPAddress;
            InterfaceAlias = $node.InterfaceAlias[1];
            AddressFamily  = $node.AddressFamily;
            DependsOn      = '[xNetAdapterName]RenameCorpnetAdapter';
        }

        Script "NewNetNat" {
            GetScript            = { return @{ Result = "" } }
            TestScript           = {
                try {
                    Get-NetNat -Name NATNetwork -ErrorAction Stop | Out-Null
                    return $true
                }
                catch {
                    return $false
                }
            }
            SetScript            = {
                New-NetNat -Name NATNetwork -InternalIPInterfaceAddressPrefix "10.0.0.0/24"
            }
            PsDscRunAsCredential = $Credential
            DependsOn            = '[xIPAddress]CorpnetIPAddress';
        }
    }

    node $AllNodes.Where( {$_.Role -NotIn 'EDGE'}).NodeName {

        xIPAddress 'PrimaryIPAddress' {
            IPAddress      = $node.CorpnetIPAddress
            InterfaceAlias = $node.InterfaceAlias
            AddressFamily  = $node.AddressFamily
        }

        xDnsServerAddress 'PrimaryDNSClient' {
            Address        = $node.DnsServerAddress;
            InterfaceAlias = $node.InterfaceAlias;
            AddressFamily  = $node.AddressFamily;
            DependsOn      = '[xIPAddress]PrimaryIPAddress';
        }

        # Do not set the default gateway for the EDGE server to avoid errors like
        # 'New-NetRoute : Instance MSFT_NetRoute already exists'
        # When this configuration was part of the .Where({$true}) stanza above,
        # I got those errors on EDGE all the time.
        xDefaultGatewayAddress 'NonEdgePrimaryDefaultGateway' {
            InterfaceAlias = $node.InterfaceAlias;
            Address        = $node.DefaultGateway;
            AddressFamily  = $node.AddressFamily;
            DependsOn      = '[xIPAddress]PrimaryIPAddress';
        }

    }

    node $Allnodes.Where( {'Firefox' -in $_.Lability_Resource}).NodeName {
        Script "InstallFirefox" {
            GetScript            = { return @{ Result = "" } }
            TestScript           = {
                Test-Path -Path "C:\Program Files\Mozilla Firefox"
            }
            SetScript            = {
                $process = Start-Process -FilePath "C:\Resources\Firefox-Latest.exe" -Wait -PassThru -ArgumentList @('-ms')
                if ($process.ExitCode -ne 0) {
                    throw "Firefox installer at $ffInstaller exited with code $($process.ExitCode)"
                }
            }
            PsDscRunAsCredential = $Credential
        }
    }

}

Deploy-NATNET.ps1

[CmdletBinding()] Param(
    [SecureString] $AdminPassword = (Read-Host -AsSecureString -Prompt "Admin password"),
    [string] $ConfigurationData = (Join-Path -Path $PSScriptRoot -ChildPath ConfigurationData.NATNET.psd1),
    [string] $ConfigureScript = (Join-Path -Path $PSScriptRoot -ChildPath Configure.NATNET.ps1),
    [string] $DscConfigName = "NatNetwork",
    [switch] $IgnorePendingReboot
)

$ErrorActionPreference = "Stop"

. $ConfigureScript
& $DscConfigName -ConfigurationData $ConfigurationData -OutputPath $env:LabilityConfigurationPath -Verbose
Start-LabConfiguration -ConfigurationData $ConfigurationData -Path $env:LabilityConfigurationPath -Verbose -Password $AdminPassword -IgnorePendingReboot:$IgnorePendingReboot
Start-Lab -ConfigurationData $ConfigurationData -Verbose