Subnet.psm1

function Get-NetworkClass {
    <#
        .SYNOPSIS
            Use to determine the network class of a given IP address.
 
        .DESCRIPTION
            Returns A, B, C, D or E depending on the numeric value of the first octet of a given IP address.
 
        .PARAMETER IP
            The IP address to test.
 
        .EXAMPLE
            Get-NetworkClass -IP 172.16.1.2
 
            Result
            ------
            B
 
        .EXAMPLE
            '10.1.1.1' | Get-NetworkClass
 
            Result
            ------
            A
    #>

    param(
        [parameter(Mandatory,ValueFromPipeline)]
        [string]
        $IP
    )
    process {

        switch ($IP.Split('.')[0]) {
            { $_ -in 0..127 } { 'A' }
            { $_ -in 128..191 } { 'B' }
            { $_ -in 192..223 } { 'C' }
            { $_ -in 224..239 } { 'D' }
            { $_ -in 240..255 } { 'E' }
        }
    }
}
function Get-Subnet {
    <#
        .SYNOPSIS
            Returns subnet details for the local IP address, or a given network address and mask.
 
        .DESCRIPTION
            Use to get subnet details for a given network address and mask, including network address, broadcast address, network class, address range, host addresses and host address count.
 
        .PARAMETER IP
            The network IP address or IP address with subnet mask via slash notation.
 
        .PARAMETER MaskBits
            The numerical representation of the subnet mask.
 
        .PARAMETER Force
            Use to force the return of all host IP addresses regardless of the subnet size (skipped by default for subnets larger than /16).
 
        .EXAMPLE
            Get-Subnet 10.1.2.3/24
 
            Description
            -----------
            Returns the subnet details for the specified network and mask, specified as a single string to the -IP parameter.
 
        .EXAMPLE
            Get-Subnet 192.168.0.1 -MaskBits 23
 
            Description
            -----------
            Returns the subnet details for the specified network and mask.
 
        .EXAMPLE
            Get-Subnet
 
            Description
            -----------
            Returns the subnet details for the current local IP.
 
        .EXAMPLE
            '10.1.2.3/24','10.1.2.4/24' | Get-Subnet
 
            Description
            -----------
            Returns the subnet details for two specified networks.
    #>

    param ( 
        [parameter(ValueFromPipeline)]
        [string]
        $IP,

        [ValidateRange(0, 32)]
        [Alias('CIDR')]
        [int]
        $MaskBits,

        [switch]
        $Force
    )
    process {

        if ($PSBoundParameters.ContainsKey('MaskBits')) { 
            $Mask = $MaskBits 
        }

        if (-not $IP) {
            $LocalIP = Get-LocalIPv4Address

            if (-not $LocalIP) {
                throw "Unable to determine a local IPv4 address for this system. Please specify the -IP parameter explicitly."
            }

            $IP = $LocalIP.IPAddress
            If ($Mask -notin 0..32) { $Mask = $LocalIP.PrefixLength }
        }

        if ($IP -match '/\d') {
            $IPandMask = $IP -Split '/'
            $IP = $IPandMask[0]
            # Without this cast, $Mask stays a string here, and PowerShell's comparison operators then
            # compare it lexicographically rather than numerically -- e.g. '8' -ge 16 is $true, because
            # '8' -ge 16 is stringwise, so '8' > '1'. That silently mis-triggers (or skips) the -ge 16 /
            # -ge 31 checks below for any single-digit mask (/0 - /9).
            $Mask = [int]$IPandMask[1]
        }
        
        $Class = Get-NetworkClass -IP $IP

        if ($Mask -notin 0..32) {

            $Mask = switch ($Class) {
                'A' { 8 }
                'B' { 16 }
                'C' { 24 }
                default { 
                    throw "Subnet mask size was not specified and could not be inferred because the address is Class $Class." 
                }
            }

            Write-Warning "Subnet mask size was not specified. Using default subnet size for a Class $Class network of /$Mask."
        }

        $IPAddr = [ipaddress]::Parse($IP)
        $MaskAddr = [ipaddress]::Parse((Convert-Int64toIP -int ([convert]::ToInt64(("1" * $Mask + "0" * (32 - $Mask)), 2))))        
        $NetworkAddr = [ipaddress]($MaskAddr.address -band $IPAddr.address) 
        $BroadcastAddr = [ipaddress](([ipaddress]::parse("255.255.255.255").address -bxor $MaskAddr.address -bor $NetworkAddr.address))
        $Range = "$NetworkAddr ~ $BroadcastAddr"
        
        $HostStartAddr = (Convert-IPtoInt64 -ip $NetworkAddr.ipaddresstostring) + 1
        $HostEndAddr = (Convert-IPtoInt64 -ip $broadcastaddr.ipaddresstostring) - 1

        if ($Mask -ge 16 -or $Force) {
            
            Write-Progress "Calcualting host addresses for $NetworkAddr/$Mask.."
            if ($Mask -ge 31) {
                $HostAddresses = ,$NetworkAddr
                if ($Mask -eq 31) {
                    $HostAddresses += $BroadcastAddr
                }

                $HostAddressCount = $HostAddresses.Length
                $NetworkAddr = $null
                $BroadcastAddr = $null
            } else {
                $HostAddresses = for ($i = $HostStartAddr; $i -le $HostEndAddr; $i++) {
                    Convert-Int64toIP -int $i
                }
                $HostAddressCount = ($HostEndAddr - $HostStartAddr) + 1
            }                     
        }
        else {
            Write-Warning "Host address enumeration was not performed because it would take some time for a /$Mask subnet. `nUse -Force if you want it to occur."
        }

        [pscustomobject]@{
            IPAddress        = $IPAddr
            MaskBits         = $Mask
            NetworkAddress   = $NetworkAddr
            BroadcastAddress = $broadcastaddr
            SubnetMask       = $MaskAddr
            NetworkClass     = $Class
            Range            = $Range
            HostAddresses    = $HostAddresses
            HostAddressCount = $HostAddressCount
        }
    }
}
function Test-PrivateIP {
    <#
        .SYNOPSIS
            Use to determine if a given IP address is within the IPv4 private address space ranges.
 
        .DESCRIPTION
            Returns $true or $false for a given IP address string depending on whether or not is is within the private IP address ranges.
 
        .PARAMETER IP
            The IP address to test.
 
        .EXAMPLE
            Test-PrivateIP -IP 172.16.1.2
 
            Result
            ------
            True
    #>

    param(
        [parameter(Mandatory,ValueFromPipeline)]
        [string]
        $IP
    )
    process {

        if ($IP -Match '(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)') {
            $true
        }
        else {
            $false
        }
    }    
}
function Convert-Int64toIP ([int64]$int) {
    <#
        .SYNOPSIS
            Converts a 32-bit integer to its dotted-decimal IPv4 address string representation.
    #>

    (([math]::truncate($int / 16777216)).tostring() + "." + ([math]::truncate(($int % 16777216) / 65536)).tostring() + "." + ([math]::truncate(($int % 65536) / 256)).tostring() + "." + ([math]::truncate($int % 256)).tostring() )
}

function Convert-IPtoInt64 ($ip) {
    <#
        .SYNOPSIS
            Converts a dotted-decimal IPv4 address string to its 32-bit integer representation.
    #>

    $octets = $ip.split(".")
    [int64]([int64]$octets[0] * 16777216 + [int64]$octets[1] * 65536 + [int64]$octets[2] * 256 + [int64]$octets[3])
}

function Convert-SubnetMaskToPrefixLength ([System.Net.IPAddress]$SubnetMask) {
    <#
        .SYNOPSIS
            Converts a dotted-decimal subnet mask (e.g. 255.255.255.0) to its prefix length in bits (e.g. 24).
    #>

    $Bits = ($SubnetMask.GetAddressBytes() | ForEach-Object { [Convert]::ToString($_, 2).PadLeft(8, '0') }) -join ''
    ($Bits.ToCharArray() | Where-Object { $_ -eq '1' }).Count
}

function Get-LocalIPv4Address {
    <#
        .SYNOPSIS
            Returns the IPv4 address and prefix length of the first active, non-loopback network interface.
 
        .NOTES
            [System.Net.NetworkInformation.NetworkInterface] is part of the base class library, so unlike
            Get-NetIPAddress (Windows-only) this works the same way on Windows, Linux and macOS.
    #>

    $UnicastAddress = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() |
        Where-Object { $_.OperationalStatus -eq 'Up' -and $_.NetworkInterfaceType -ne 'Loopback' } |
        ForEach-Object { $_.GetIPProperties().UnicastAddresses } |
        Where-Object { $_.Address.AddressFamily -eq 'InterNetwork' } |
        Select-Object -First 1

    if (-not $UnicastAddress) {
        return
    }

    # .PrefixLength isn't implemented on .NET Framework (Windows PowerShell), so fall back to counting
    # the bits of .IPv4Mask there instead -- and the reverse is true on Linux/macOS, where .IPv4Mask
    # throws because those platforms have no concept of a dotted-decimal subnet mask.
    $PrefixLength = try {
        $UnicastAddress.PrefixLength
    }
    catch {
        Convert-SubnetMaskToPrefixLength -SubnetMask $UnicastAddress.IPv4Mask
    }

    [pscustomobject]@{
        IPAddress    = $UnicastAddress.Address.IPAddressToString
        PrefixLength = $PrefixLength
    }
}