Indented.Net.IP.psm1

function ConvertToNetwork {
    <#
    .SYNOPSIS
        Converts IP address formats to a set a known styles.
    .DESCRIPTION
        ConvertToNetwork ensures consistent values are recorded from parameters which must handle differing addressing formats. This Cmdlet allows all other the other functions in this module to offload parameter handling.
    .NOTES
        Change log:
            05/03/2016 - Chris Dent - Refactored and simplified.
            14/01/2014 - Chris Dent - Created.
    #>


    [CmdletBinding()]
    [OutputType('Indented.Net.IP.Network')]
    param (
        # Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string.
        [Parameter(Mandatory = $true, Position = 1)]
        [String]$IPAddress,

        # A subnet mask as an IP address.
        [Parameter(Position = 2)]
        [AllowNull()]
        [String]$SubnetMask
    )

    $validSubnetMaskValues =
        "0.0.0.0", "128.0.0.0", "192.0.0.0",
        "224.0.0.0", "240.0.0.0", "248.0.0.0", "252.0.0.0",
        "254.0.0.0", "255.0.0.0", "255.128.0.0", "255.192.0.0",
        "255.224.0.0", "255.240.0.0", "255.248.0.0", "255.252.0.0",
        "255.254.0.0", "255.255.0.0", "255.255.128.0", "255.255.192.0",
        "255.255.224.0", "255.255.240.0", "255.255.248.0", "255.255.252.0",
        "255.255.254.0", "255.255.255.0", "255.255.255.128", "255.255.255.192",
        "255.255.255.224", "255.255.255.240", "255.255.255.248", "255.255.255.252",
        "255.255.255.254", "255.255.255.255"

    $network = [PSCustomObject]@{
        IPAddress  = $null
        SubnetMask = $null
        MaskLength = 0
        PSTypeName = 'Indented.Net.IP.Network'
    }

    # Override ToString
    $network | Add-Member ToString -MemberType ScriptMethod -Force -Value {
        '{0}/{1}' -f $this.IPAddress, $this.MaskLength
    }

    if (-not $psboundparameters.ContainsKey('SubnetMask') -or $SubnetMask -eq '') {
        $IPAddress, $SubnetMask = $IPAddress.Split([Char[]]'\/ ', [StringSplitOptions]::RemoveEmptyEntries)
    }

    # IPAddress

    while ($IPAddress.Split('.').Count -lt 4) {
        $IPAddress += '.0'
    }

    if ([IPAddress]::TryParse($IPAddress, [Ref]$null)) {
        $network.IPAddress = [IPAddress]$IPAddress
    } else {
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            [ArgumentException]'Invalid IP address.',
            'InvalidIPAddress',
            'InvalidArgument',
            $IPAddress
        )
        $pscmdlet.ThrowTerminatingError($errorRecord)
    }

    # SubnetMask

    if ($null -eq $SubnetMask -or $SubnetMask -eq '') {
        $network.SubnetMask = [IPAddress]$validSubnetMaskValues[32]
        $network.MaskLength = 32
    } else {
        $maskLength = 0
        if ([Int32]::TryParse($SubnetMask, [Ref]$maskLength)) {
            if ($MaskLength -ge 0 -and $maskLength -le 32) {
                $network.SubnetMask = [IPAddress]$validSubnetMaskValues[$maskLength]
                $network.MaskLength = $maskLength
            } else {
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    [ArgumentException]'Mask length out of range (expecting 0 to 32).',
                    'InvalidMaskLength',
                    'InvalidArgument',
                    $SubnetMask
                )
                $pscmdlet.ThrowTerminatingError($errorRecord)
            }
        } else {
            while ($SubnetMask.Split('.').Count -lt 4) {
                $SubnetMask += '.0'
            }
            $maskLength = $validSubnetMaskValues.IndexOf($SubnetMask)

            if ($maskLength -ge 0) {
                $Network.SubnetMask = [IPAddress]$SubnetMask
                $Network.MaskLength = $maskLength
            } else {
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    [ArgumentException]'Invalid subnet mask.',
                    'InvalidSubnetMask',
                    'InvalidArgument',
                    $SubnetMask
                )
                $pscmdlet.ThrowTerminatingError($errorRecord)
            }
        }
    }

    $network
}

function GetPermutation {
    <#
    .SYNOPSIS
        Gets permutations of an IP address expansion expression.
    .DESCRIPTION
        Gets permutations of an IP address expansion expression.
    #>


    [CmdletBinding()]
    param (
        [PSTypeName('ExpansionGroupInfo')]
        [Object[]]$Group,

        [String]$BaseAddress,

        [Int32]$Index
    )

    foreach ($value in $Group[$Index].ReplaceWith) {
        $octets = $BaseAddress -split '\.'
        $octets[$Group[$Index].Position] = $value
        $address = $octets -join '.'

        if ($Index -lt $Group.Count - 1) {
            $address = GetPermutation $Group -Index ($Index + 1) -BaseAddress $address
        }
        $address
    }
}

function ConvertFrom-HexIP {
    <#
    .SYNOPSIS
        Converts a hexadecimal IP address into a dotted decimal string.
    .DESCRIPTION
        ConvertFrom-HexIP takes a hexadecimal string and returns a dotted decimal IP address. An intermediate call is made to ConvertTo-DottedDecimalIP.
    .INPUTS
        System.String
    .EXAMPLE
        ConvertFrom-HexIP c0a80001
 
        Returns the IP address 192.168.0.1.
    #>


    [CmdletBinding()]
    [OutputType([IPAddress])]
    param (
        # An IP Address to convert.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [ValidatePattern('^(0x)?[0-9a-f]{8}$')]
        [String]$IPAddress
    )

    process {
        [IPAddress][UInt64][Convert]::ToUInt32($IPAddress, 16)
    }
}

function ConvertTo-BinaryIP {
    <#
    .SYNOPSIS
        Converts a Decimal IP address into a binary format.
    .DESCRIPTION
        ConvertTo-BinaryIP uses System.Convert to switch between decimal and binary format. The output from this function is dotted binary.
    .INPUTS
        System.Net.IPAddress
    .EXAMPLE
        ConvertTo-BinaryIP 1.2.3.4
 
        Convert an IP address to a binary format.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    param (
        # An IP Address to convert.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [IPAddress]$IPAddress
    )

    process {
        $binary = foreach ($byte in $IPAddress.GetAddressBytes()) {
            [Convert]::ToString($byte, 2).PadLeft(8, '0')
        }
        $binary -join '.'
    }
}

function ConvertTo-DecimalIP {
    <#
    .SYNOPSIS
        Converts a Decimal IP address into a 32-bit unsigned integer.
    .DESCRIPTION
        ConvertTo-DecimalIP takes a decimal IP, uses a shift operation on each octet and returns a single UInt32 value.
    .INPUTS
        System.Net.IPAddress
    .EXAMPLE
        ConvertTo-DecimalIP 1.2.3.4
 
        Converts an IP address to an unsigned 32-bit integer value.
    #>


    [CmdletBinding()]
    [OutputType([UInt32])]
    param (
        # An IP Address to convert.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline )]
        [IPAddress]$IPAddress
    )

    process {
        [UInt32]([IPAddress]::HostToNetworkOrder($IPAddress.Address) -shr 32 -band [UInt32]::MaxValue)
    }
}

function ConvertTo-DottedDecimalIP {
    <#
    .SYNOPSIS
        Converts either an unsigned 32-bit integer or a dotted binary string to an IP Address.
    .DESCRIPTION
         ConvertTo-DottedDecimalIP uses a regular expression match on the input string to convert to an IP address.
    .INPUTS
        System.String
    .EXAMPLE
        ConvertTo-DottedDecimalIP 11000000.10101000.00000000.00000001
 
        Convert the binary form back to dotted decimal, resulting in 192.168.0.1.
    .EXAMPLE
        ConvertTo-DottedDecimalIP 3232235521
 
        Convert the decimal form back to dotted decimal, resulting in 192.168.0.1.
    #>


    [CmdletBinding()]
    [OutputType([IPAddress])]
    param (
        # A string representation of an IP address from either UInt32 or dotted binary.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [String]$IPAddress
    )

    process {
        try {
            [Int64]$value = 0
            if ([Int64]::TryParse($IPAddress, [Ref]$value)) {
                return [IPAddress]([IPAddress]::NetworkToHostOrder([Int64]$value) -shr 32 -band [UInt32]::MaxValue)
            } else {
                [IPAddress][UInt64][Convert]::ToUInt32($IPAddress.Replace('.', ''), 2)
            }
        } catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [ArgumentException]'Cannot convert this format.',
                'UnrecognisedFormat',
                'InvalidArgument',
                $IPAddress
            )
            Write-Error -ErrorRecord $errorRecord
        }
    }
}

function ConvertTo-HexIP {
    <#
    .SYNOPSIS
        Convert a dotted decimal IP address into a hexadecimal string.
    .DESCRIPTION
        ConvertTo-HexIP takes a dotted decimal IP and returns a single hexadecimal string value.
    .PARAMETER IPAddress
        An IP Address to convert.
    .INPUTS
        System.Net.IPAddress
    .EXAMPLE
        ConvertTo-HexIP 192.168.0.1
 
        Returns the hexadecimal string c0a80001.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [IPAddress]$IPAddress
    )

    process {
        $bytes = $IPAddress.GetAddressBytes()
        [Array]::Reverse($bytes)
        '{0:x8}' -f [BitConverter]::ToUInt32($bytes, 0)
    }
}

function ConvertTo-Mask {
    <#
    .SYNOPSIS
        Convert a mask length to a dotted-decimal subnet mask.
    .DESCRIPTION
        ConvertTo-Mask returns a subnet mask in dotted decimal format from an integer value ranging between 0 and 32.
 
        ConvertTo-Mask creates a binary string from the length, converts the string to an unsigned 32-bit integer then calls ConvertTo-DottedDecimalIP to complete the operation.
    .INPUTS
        System.Int32
    .EXAMPLE
        ConvertTo-Mask 24
 
        Returns the dotted-decimal form of the mask, 255.255.255.0.
    #>


    [CmdletBinding()]
    [OutputType([IPAddress])]
    param (
        # The number of bits which must be masked.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [Alias('Length')]
        [ValidateRange(0, 32)]
        [Byte]$MaskLength
    )

    process {
        [IPAddress][UInt64][Convert]::ToUInt32(('1' * $MaskLength).PadRight(32, '0'), 2)
    }
}

function ConvertTo-MaskLength {
    <#
    .SYNOPSIS
        Convert a dotted-decimal subnet mask to a mask length.
    .DESCRIPTION
        A count of the number of 1's in a binary string.
    .INPUTS
        System.Net.IPAddress
    .EXAMPLE
        ConvertTo-MaskLength 255.255.255.0
 
        Returns 24, the length of the mask in bits.
    #>


    [CmdletBinding()]
    [OutputType([Int32])]
    param (
        # A subnet mask to convert into length.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [Alias("Mask")]
        [IPAddress]$SubnetMask
    )

    process {
        [Convert]::ToString([IPAddress]::HostToNetworkOrder($SubnetMask.Address), 2).Replace('0', '').Length
    }
}

function ConvertTo-Subnet {
    <#
    .SYNOPSIS
        Convert a start and end IP address to the closest matching subnet.
    .DESCRIPTION
        ConvertTo-Subnet attempts to convert a starting and ending IP address from a range to the closest subnet.
    .EXAMPLE
        ConvertTo-Subnet -Start 0.0.0.0 -End 255.255.255.255
    .EXAMPLE
        ConvertTo-Subnet -Start 192.168.0.1 -End 192.168.0.129
    .EXAMPLE
        ConvertTo-Subnet 10.0.0.23/24
    .EXAMPLE
        ConvertTo-Subnet 10.0.0.23 255.255.255.0
    #>


    [CmdletBinding(DefaultParameterSetName = 'FromIPAndMask')]
    [OutputType('Indented.Net.IP.Subnet')]
    param (
        # Any IP address in the subnet.
        [Parameter(Mandatory, Position = 1, ParameterSetName = 'FromIPAndMask')]
        [String]$IPAddress,

        # A subnet mask.
        [Parameter(Position = 2, ParameterSetName = 'FromIPAndMask')]
        [String]$SubnetMask,

        # The first IP address from a range.
        [Parameter(Mandatory, ParameterSetName = 'FromStartAndEnd')]
        [IPAddress]$Start,

        # The last IP address from a range.
        [Parameter(Mandatory, ParameterSetName = 'FromStartAndEnd')]
        [IPAddress]$End
    )

    if ($pscmdlet.ParameterSetName -eq 'FromIPAndMask') {
        try {
            $network = ConvertToNetwork @psboundparameters
        } catch {
            $pscmdlet.ThrowTerminatingError($_)
        }
    } elseif ($pscmdlet.ParameterSetName -eq 'FromStartAndEnd') {
        if ($Start -eq $End) {
            $MaskLength = 32
        } else {
            $DecimalStart = ConvertTo-DecimalIP $Start
            $DecimalEnd = ConvertTo-DecimalIP $End

            if ($DecimalEnd -lt $DecimalStart) {
                $Start = $End
            }

            # Find the point the binary representation of each IP address diverges
            $i = 32
            do {
                $i--
            } until (($DecimalStart -band ([UInt32]1 -shl $i)) -ne ($DecimalEnd -band ([UInt32]1 -shl $i)))

            $MaskLength = 32 - $i - 1
        }

        try {
            $network = ConvertToNetwork $Start $MaskLength
        } catch {
            $pscmdlet.ThrowTerminatingError($_)
        }
    }

    $hostAddresses = [Math]::Pow(2, (32 - $network.MaskLength)) - 2
    if ($hostAddresses -lt 0) {
        $hostAddresses = 0
    }

    $subnet = [PSCustomObject]@{
        NetworkAddress   = Get-NetworkAddress $network.ToString()
        BroadcastAddress = Get-BroadcastAddress $network.ToString()
        SubnetMask       = $network.SubnetMask
        MaskLength       = $network.MaskLength
        HostAddresses    = $hostAddresses
        PSTypeName       = 'Indented.Net.IP.Subnet'
    }

    $subnet | Add-Member ToString -MemberType ScriptMethod -Force -Value {
        return '{0}/{1}' -f $this.NetworkAddress, $this.MaskLength
    }

    $subnet
}

function Get-BroadcastAddress {
    <#
    .SYNOPSIS
        Get the broadcast address for a network range.
    .DESCRIPTION
        Get-BroadcastAddress returns the broadcast address for a subnet by performing a bitwise AND operation against the decimal forms of the IP address and inverted subnet mask.
    .INPUTS
        System.String
    .EXAMPLE
        Get-BroadcastAddress 192.168.0.243 255.255.255.0
 
        Returns the address 192.168.0.255.
    .EXAMPLE
        Get-BroadcastAddress 10.0.9/22
 
        Returns the address 10.0.11.255.
    .EXAMPLE
        Get-BroadcastAddress 0/0
 
        Returns the address 255.255.255.255.
    .EXAMPLE
        Get-BroadcastAddress "10.0.0.42 255.255.255.252"
 
        Input values are automatically split into IP address and subnet mask. Returns the address 10.0.0.43.
    #>


    [CmdletBinding()]
    [OutputType([IPAddress])]
    param (
        # Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [String]$IPAddress,

        # A subnet mask as an IP address.
        [Parameter(Position = 2)]
        [String]$SubnetMask
    )

    process {
        try {
            $network = ConvertToNetwork @psboundparameters

            $networkAddress = [IPAddress]($network.IPAddress.Address -band $network.SubnetMask.Address)

            return [IPAddress](
                $networkAddress.Address -bor
                -bnot $network.SubnetMask.Address -band
                -bnot ([Int64][UInt32]::MaxValue -shl 32)
            )
        } catch {
            Write-Error -ErrorRecord $_
        }
    }
}

function Get-NetworkAddress {
    <#
    .SYNOPSIS
        Get the network address for a network range.
    .DESCRIPTION
        Get-NetworkAddress returns the network address for a subnet by performing a bitwise AND operation against the decimal forms of the IP address and subnet mask.
    .PARAMETER IPAddress
        Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string.
    .PARAMETER SubnetMask
        A subnet mask as an IP address.
    .INPUTS
        System.String
    .EXAMPLE
        Get-NetworkAddress 192.168.0.243 255.255.255.0
 
        Returns the address 192.168.0.0.
    .EXAMPLE
        Get-NetworkAddress 10.0.9/22
 
        Returns the address 10.0.8.0.
    .EXAMPLE
        Get-NetworkAddress "10.0.23.21 255.255.255.224"
 
        Input values are automatically split into IP address and subnet mask. Returns the address 10.0.23.0.
    #>


    [CmdletBinding()]
    [OutputType([IPAddress])]
    param (
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [String]$IPAddress,

        [Parameter(Position = 2)]
        [String]$SubnetMask
    )

    process {
        try {
            $network = ConvertToNetwork @psboundparameters

            return [IPAddress]($network.IPAddress.Address -band $network.SubnetMask.Address)
        } catch {
            Write-Error -ErrorRecord $_
        }
    }
}

function Get-NetworkRange {
    <#
    .SYNOPSIS
        Get a list of IP addresses within the specified network.
    .DESCRIPTION
        Get-NetworkRange finds the network and broadcast address as decimal values then starts a counter between the two, returning IPAddress for each.
    .INPUTS
        System.String
    .EXAMPLE
        Get-NetworkRange 192.168.0.0 255.255.255.0
 
        Returns all IP addresses in the range 192.168.0.0/24.
    .EXAMPLE
        Get-NetworkRange 10.0.8.0/22
 
        Returns all IP addresses in the range 192.168.0.0 255.255.252.0.
    #>


    [CmdletBinding(DefaultParameterSetName = 'FromIPAndMask')]
    [OutputType([IPAddress])]
    param (
        # Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline, ParameterSetName = 'FromIPAndMask')]
        [String]$IPAddress,

        # A subnet mask as an IP address.
        [Parameter(Position = 2, ParameterSetName = 'FromIPAndMask')]
        [String]$SubnetMask,

        # Include the network and broadcast addresses when generating a network address range.
        [Parameter(ParameterSetName = 'FromIPAndMask')]
        [Switch]$IncludeNetworkAndBroadcast,

        # The start address of a range.
        [Parameter(Mandatory, ParameterSetName = 'FromStartAndEnd')]
        [IPAddress]$Start,

        # The end address of a range.
        [Parameter(Mandatory, ParameterSetName = 'FromStartAndEnd')]
        [IPAddress]$End
    )

    process {
        if ($pscmdlet.ParameterSetName -eq 'FromIPAndMask') {
            try {
                $null = $psboundparameters.Remove('IncludeNetworkAndBroadcast')
                $network = ConvertToNetwork @psboundparameters
            } catch {
                $pscmdlet.ThrowTerminatingError($_)
            }

            $decimalIP = ConvertTo-DecimalIP $network.IPAddress
            $decimalMask = ConvertTo-DecimalIP $network.SubnetMask

            $startDecimal = $decimalIP -band $decimalMask
            $endDecimal = $decimalIP -bor (-bnot $decimalMask -band [UInt32]::MaxValue)

            if (-not $IncludeNetworkAndBroadcast) {
                $startDecimal++
                $endDecimal--
            }
        } else {
            $startDecimal = ConvertTo-DecimalIP $Start
            $endDecimal = ConvertTo-DecimalIP $End
        }

        for ($i = $startDecimal; $i -le $endDecimal; $i++) {
            [IPAddress]([IPAddress]::NetworkToHostOrder([Int64]$i) -shr 32 -band [UInt32]::MaxValue)
        }
    }
}

function Get-NetworkSummary {
    <#
    .SYNOPSIS
        Generates a summary describing several properties of a network range
    .DESCRIPTION
        Get-NetworkSummary uses many of the IP conversion commands to provide a summary of a network range from any IP address in the range and a subnet mask.
    .INPUTS
        System.String
    .EXAMPLE
        Get-NetworkSummary 192.168.0.1 255.255.255.0
    .EXAMPLE
        Get-NetworkSummary 10.0.9.43/22
    .EXAMPLE
        Get-NetworkSummary 0/0
    #>


    [CmdletBinding()]
    [OutputType('Indented.Net.IP.NetworkSummary')]
    param (
        # Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [String]$IPAddress,

        # A subnet mask as an IP address.
        [Parameter(Position = 2)]
        [String]$SubnetMask
    )

    process {
        try {
            $network = ConvertToNetwork @psboundparameters
        } catch {
            throw $_
        }

        $decimalIP = ConvertTo-DecimalIP $Network.IPAddress
        $decimalMask = ConvertTo-DecimalIP $Network.SubnetMask
        $decimalNetwork =  $decimalIP -band $decimalMask
        $decimalBroadcast = $decimalIP -bor (-bnot $decimalMask -band [UInt32]::MaxValue)

        $networkSummary = [PSCustomObject]@{
            NetworkAddress    = $networkAddress = ConvertTo-DottedDecimalIP $decimalNetwork
            NetworkDecimal    = $decimalNetwork
            BroadcastAddress  = ConvertTo-DottedDecimalIP $decimalBroadcast
            BroadcastDecimal  = $decimalBroadcast
            Mask              = $network.SubnetMask
            MaskLength        = $maskLength = ConvertTo-MaskLength $network.SubnetMask
            MaskHexadecimal   = ConvertTo-HexIP $network.SubnetMask
            CIDRNotation      = '{0}/{1}' -f $networkAddress, $maskLength
            HostRange         = ''
            NumberOfAddresses = $decimalBroadcast - $decimalNetwork + 1
            NumberOfHosts     = $decimalBroadcast - $decimalNetwork - 1
            Class             = ''
            IsPrivate         = $false
            PSTypeName        = 'Indented.Net.IP.NetworkSummary'
        }

        if ($networkSummary.NumberOfHosts -lt 0) {
            $networkSummary.NumberOfHosts = 0
        }
        if ($networkSummary.MaskLength -lt 31) {
            $networkSummary.HostRange = '{0} - {1}' -f @(
                (ConvertTo-DottedDecimalIP ($decimalNetwork + 1))
                (ConvertTo-DottedDecimalIP ($decimalBroadcast - 1))
            )
        }

        $networkSummary.Class = switch -regex (ConvertTo-BinaryIP $network.IPAddress) {
            '^1111'               { 'E'; break }
            '^1110'               { 'D'; break }
            '^11000000\.10101000' { if ($networkSummary.MaskLength -ge 16) { $networkSummary.IsPrivate = $true } }
            '^110'                { 'C'; break }
            '^10101100\.0001'     { if ($networkSummary.MaskLength -ge 12) { $networkSummary.IsPrivate = $true } }
            '^10'                 { 'B'; break }
            '^00001010'           { if ($networkSummary.MaskLength -ge 8) { $networkSummary.IsPrivate = $true} }
            '^0'                  { 'A'; break }
        }

        $networkSummary
    }
}

function Get-Subnet {
    <#
    .SYNOPSIS
        Get a list of subnets of a given size within a defined supernet.
    .DESCRIPTION
        Generates a list of subnets for a given network range using either the address class or a user-specified value.
    .EXAMPLE
        Get-Subnet 10.0.0.0 255.255.255.0 -NewSubnetMask 255.255.255.192
 
        Four /26 networks are returned.
    .EXAMPLE
        Get-Subnet 0/22 -NewSubnetMask 24
 
        64 /24 networks are returned.
    .NOTES
        Change log:
            07/03/2016 - Chris Dent - Cleaned up code, added tests.
            12/12/2015 - Chris Dent - Redesigned.
            13/10/2011 - Chris Dent - Created.
    #>


    [CmdletBinding()]
    [OutputType('Indented.Net.IP.Subnet')]
    param (
        # Any address in the super-net range. Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string.
        [Parameter(Mandatory = $true, Position = 1)]
        [String]$IPAddress,

        # The subnet mask of the network to split. Mandatory if the subnet mask is not included in the IPAddress parameter.
        [Parameter(Position = 2)]
        [String]$SubnetMask,

        # Split the existing network described by the IPAddress and subnet mask using this mask.
        [Parameter(Mandatory = $true)]
        [String]$NewSubnetMask
    )

    $null = $psboundparameters.Remove('NewSubnetMask')
    try {
        $network = ConvertToNetwork @psboundparameters
        $newNetwork = ConvertToNetwork 0 $NewSubnetMask
    } catch {
        $pscmdlet.ThrowTerminatingError($_)
    }

    if ($network.MaskLength -gt $newNetwork.MaskLength) {
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            [ArgumentException]'The subnet mask of the new network is shorter (masks fewer addresses) than the subnet mask of the existing network.',
            'NewSubnetMaskTooShort',
            'InvalidArgument',
            $NewNetwork.MaskLength
        )
        $pscmdlet.ThrowTerminatingError($errorRecord)
    }

    $numberOfNets = [Math]::Pow(2, ($newNetwork.MaskLength - $network.MaskLength))
    $numberOfAddresses = [Math]::Pow(2, (32 - $newNetwork.MaskLength))

    $decimalAddress = ConvertTo-DecimalIP (Get-NetworkAddress $network.ToString())
    for ($i = 0; $i -lt $numberOfNets; $i++) {
        $networkAddress = ConvertTo-DottedDecimalIP $decimalAddress

        ConvertTo-Subnet -IPAddress $networkAddress -SubnetMask $newNetwork.MaskLength

        $decimalAddress += $numberOfAddresses
    }
}

function Resolve-IPAddress {
    <#
    .SYNOPSIS
        Resolves an IP address expression using wildcard expressions to individual IP addresses.
    .DESCRIPTION
        Resolves an IP address expression using wildcard expressions to individual IP addresses.
 
        Resolve-IPAddress expands groups and values in square brackets to generate a list of IP addresses or networks using CIDR-notation.
 
        Ranges of values may be specied using a start and end value using "-" to separate the values.
 
        Specific values may be listed as a comma separated list.
    .EXAMPLE
        Resolve-IPAddress "10.[1,2].[0-2].0/24"
 
        Returns the addresses 10.1.0.0/24, 10.1.1.0/24, 10.1.2.0/24, 10.2.0.0/24, and so on.
    #>


    [CmdletBinding()]
    param (
        # The IPAddress expression to resolve.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [String]$IPAddress
    )

    process {
        $groups = [Regex]::Matches($IPAddress, '\[(?:(?<Range>\d+(?:-\d+))|(?<Selected>(?:\d+, *)*\d+))\]|(?<All>\*)').Groups.Captures |
            Where-Object { $_ -and $_.Name -ne '0' } |
            ForEach-Object {
                $group = $_

                $values = switch ($group.Name) {
                    'Range'    {
                        [int]$start, [int]$end = $group.Value -split '-'

                        if ($start, $end -gt 255) {
                            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                                [ArgumentException]::new('Value ranges to resolve must use a start and end values between 0 and 255'),
                                'RangeExpressionOutOfRange',
                                'InvalidArgument',
                                $group.Value
                            )
                            $pscmdlet.ThrowTerminatingError($errorRecord)
                        }

                        $start..$end
                    }
                    'Selected' {
                        $values = [int[]]($group.Value -split ', *')

                        if ($values -gt 255) {
                            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                                [ArgumentException]::new('All selected values must be between 0 and 255'),
                                'SelectionExpressionOutOfRange',
                                'InvalidArgument',
                                $group.Value
                            )
                            $pscmdlet.ThrowTerminatingError($errorRecord)
                        }

                        $values
                    }
                    'All' {
                        0..255
                    }
                }

                [PSCustomObject]@{
                    Name        = $_.Name
                    Position    = [Int32]$IPAddress.Substring(0, $_.Index).Split('.').Count - 1
                    ReplaceWith = $values
                    PSTypeName  = 'ExpansionGroupInfo'
                }
            }

        if ($groups) {
            GetPermutation $groups -BaseAddress $IPAddress
        } elseif (-not [IPAddress]::TryParse(($IPAddress -replace '/\d+$'), [Ref]$null)) {
            Write-Warning 'The IPAddress argument is not a valid IP address and cannot be resolved'
        } else {
            Write-Debug 'No groups found to resolve'
        }
    }
}

function Test-SubnetMember {
    <#
    .SYNOPSIS
        Tests an IP address to determine if it falls within IP address range.
    .DESCRIPTION
        Test-SubnetMember attempts to determine whether or not an address or range falls within another range. The network and broadcast address are calculated the converted to decimal then compared to the decimal form of the submitted address.
    .EXAMPLE
        Test-SubnetMember -SubjectIPAddress 10.0.0.0/24 -ObjectIPAddress 10.0.0.0/16
 
        Returns true as the subject network can be contained within the object network.
    .EXAMPLE
        Test-SubnetMember -SubjectIPAddress 192.168.0.0/16 -ObjectIPAddress 192.168.0.0/24
 
        Returns false as the subject network is larger the object network.
    .EXAMPLE
        Test-SubnetMember -SubjectIPAddress 10.2.3.4/32 -ObjectIPAddress 10.0.0.0/8
 
        Returns true as the subject IP address is within the object network.
    .EXAMPLE
        Test-SubnetMember -SubjectIPAddress 255.255.255.255 -ObjectIPAddress 0/0
 
        Returns true as the subject IP address is the last in the object network range.
    #>


    [CmdletBinding()]
    [OutputType([Boolean])]
    param (
        # A representation of the subject, the network to be tested. Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string.
        [Parameter(Mandatory, Position = 1)]
        [String]$SubjectIPAddress,

        # A representation of the object, the network to test against. Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string.
        [Parameter(Mandatory, Position = 2)]
        [String]$ObjectIPAddress,

        # A subnet mask as an IP address.
        [String]$SubjectSubnetMask,

        # A subnet mask as an IP address.
        [String]$ObjectSubnetMask
    )

    try {
        $subjectNetwork = ConvertToNetwork $SubjectIPAddress $SubjectSubnetMask
        $objectNetwork = ConvertToNetwork $ObjectIPAddress $ObjectSubnetMask
    } catch {
        throw $_
    }

    # A simple check, if the mask is shorter (larger network) then it won't be a subnet of the object anyway.
    if ($subjectNetwork.MaskLength -lt $objectNetwork.MaskLength) {
        return $false
    }

    $subjectDecimalIP = ConvertTo-DecimalIP $subjectNetwork.IPAddress
    $objectDecimalNetwork = ConvertTo-DecimalIP (Get-NetworkAddress $objectNetwork)
    $objectDecimalBroadcast = ConvertTo-DecimalIP (Get-BroadcastAddress $objectNetwork)

    # If the mask is longer (smaller network), then the decimal form of the address must be between the
    # network and broadcast address of the object (the network we test against).
    if ($subjectDecimalIP -ge $objectDecimalNetwork -and $subjectDecimalIP -le $objectDecimalBroadcast) {
        return $true
    } else {
        return $false
    }
}