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 the full list of host IP addresses regardless of the subnet size (skipped by default for subnets larger than /16). HostAddressCount is always calculated and returned, regardless of subnet size or whether -Force is used. .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 { $Mask = $null if ($PSBoundParameters.ContainsKey('MaskBits')) { $Mask = $MaskBits } $Details = Resolve-Subnet -IP $IP -Mask $Mask $NetworkAddr = $Details.NetworkAddr $BroadcastAddr = $Details.BroadcastAddr $Range = "$NetworkAddr ~ $BroadcastAddr" # The count is cheap arithmetic regardless of subnet size, so it's always calculated -- it's only # the full list of individual host addresses below that's gated behind the size/-Force check. $HostAddressCount = if ($Details.Mask -eq 32) { 1 } elseif ($Details.Mask -eq 31) { 2 } else { ($Details.HostEndAddr - $Details.HostStartAddr) + 1 } if ($Details.Mask -ge 16 -or $Force) { Write-Progress "Calcualting host addresses for $NetworkAddr/$($Details.Mask).." # Get-SubnetHostAddress does the actual enumeration -- it's given the already-resolved IP/Mask # directly, so it won't re-run local-IP lookup or mask inference (and won't duplicate the # warning Resolve-Subnet already issued above if the mask had to be inferred). -AsString is # used since HostAddresses has always been a string array, unlike Get-SubnetHostAddress's own # default [ipaddress] output. $HostAddresses = @(Get-SubnetHostAddress -IP $Details.IPAddr.IPAddressToString -MaskBits $Details.Mask -AsString) if ($Details.Mask -ge 31) { $NetworkAddr = $null $BroadcastAddr = $null } } else { Write-Warning "The full list of host addresses was not returned because it would take some time for a /$($Details.Mask) subnet. `nUse -Force if you want it to occur. HostAddressCount has been calculated regardless." } [pscustomobject]@{ IPAddress = $Details.IPAddr MaskBits = $Details.Mask NetworkAddress = $NetworkAddr BroadcastAddress = $BroadcastAddr SubnetMask = $Details.MaskAddr NetworkClass = $Details.Class Range = $Range HostAddresses = $HostAddresses HostAddressCount = $HostAddressCount } } } function Get-SubnetHostAddress { <# .SYNOPSIS Returns the list of usable host IP addresses, as [ipaddress] objects, for a given network address and mask. .DESCRIPTION Unlike Get-Subnet, this always calculates and returns the full list of host addresses regardless of subnet size, since that's this cmdlet's only job. It still warns (but does not refuse) when the subnet is larger than /16, since generating the full list for a very large subnet can take some time. .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 AsString Return the host addresses as strings instead of [ipaddress] objects. .EXAMPLE Get-SubnetHostAddress 10.1.2.3/24 Description ----------- Returns the list of host addresses for the specified network and mask. .EXAMPLE Get-SubnetHostAddress 10.1.2.3/24 -AsString Description ----------- Returns the list of host addresses for the specified network and mask, as strings instead of [ipaddress] objects. .EXAMPLE Get-SubnetHostAddress -IP 192.168.0.1 -MaskBits 12 Description ----------- Returns the list of host addresses for the specified (large) network and mask. A warning is shown that this may take some time, but the addresses are still returned. .EXAMPLE '10.1.2.3/24','10.1.2.4/24' | Get-SubnetHostAddress Description ----------- Returns the list of host addresses for two specified networks. .EXAMPLE Get-Subnet 10.1.2.3/24 | Get-SubnetHostAddress Description ----------- Returns the list of host addresses for the network and mask resolved by Get-Subnet. #> param ( [parameter(ValueFromPipeline)] $IP, [ValidateRange(0, 32)] [Alias('CIDR')] [int] $MaskBits, [switch] $AsString ) process { $ProvidedMaskBits = $PSBoundParameters.ContainsKey('MaskBits') if ($IP -isnot [string]) { # Accept the object output by Get-Subnet (or anything else exposing IPAddress/MaskBits # properties) piped in directly, rather than requiring its properties be extracted manually. # $IP can't be typed [string] to enable this -- PowerShell's pipeline binder would coerce the # whole object to a string (via its default ToString()) before this code ever ran. if (-not $ProvidedMaskBits) { $MaskBits = $IP.MaskBits $ProvidedMaskBits = $true } $IP = $IP.IPAddress.ToString() } $Mask = $null if ($ProvidedMaskBits) { $Mask = $MaskBits } $Details = Resolve-Subnet -IP $IP -Mask $Mask if ($Details.Mask -lt 16) { Write-Warning "Calculating host addresses for a /$($Details.Mask) subnet may take some time." } if ($Details.Mask -ge 31) { if ($AsString) { $Details.NetworkAddr.IPAddressToString if ($Details.Mask -eq 31) { $Details.BroadcastAddr.IPAddressToString } } else { $Details.NetworkAddr if ($Details.Mask -eq 31) { $Details.BroadcastAddr } } } elseif ($AsString) { # Inlined rather than calling Convert-Int64toIP -- this loop can run millions of times for # large subnets, and paying PowerShell's per-call function dispatch overhead on every single # address measurably dominates the runtime at that scale. for ($i = $Details.HostStartAddr; $i -le $Details.HostEndAddr; $i++) { "$($i -shr 24 -band 255).$($i -shr 16 -band 255).$($i -shr 8 -band 255).$($i -band 255)" } } else { # Returns [ipaddress] objects (via the 4-byte constructor, since the byte order of the plain # int/long constructors doesn't match this loop's big-endian octet math) rather than strings. # Kept as a separate loop from the -AsString one above (rather than branching inside a single # loop) so the hot path isn't paying a per-iteration branch check on top of the per-call # dispatch overhead already called out above. for ($i = $Details.HostStartAddr; $i -le $Details.HostEndAddr; $i++) { [ipaddress]::new([byte[]](($i -shr 24 -band 255), ($i -shr 16 -band 255), ($i -shr 8 -band 255), ($i -band 255))) } } } } 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 Test-PublicIP { <# .SYNOPSIS Use to determine if a given IP address is within the IPv4 public address space. .DESCRIPTION Returns $true or $false for a given IP address string depending on whether or not it is within the public IP address space, i.e it is not within the private IP address ranges. .PARAMETER IP The IP address to test. .EXAMPLE Test-PublicIP -IP 8.8.8.8 Result ------ True #> param( [parameter(Mandatory,ValueFromPipeline)] [string] $IP ) process { -not (Test-PrivateIP -IP $IP) } } function Convert-Int64toIP ([int64]$int) { <# .SYNOPSIS Converts a 32-bit integer to its dotted-decimal IPv4 address string representation. .NOTES Uses bit-shifting rather than division -- PowerShell's '/' operator always promotes to floating point, so the previous [math]::truncate($int / 16777216) approach paid for a double conversion on every octet. Bitwise ops stay in integer math throughout, which matters here since this can run in a tight loop generating millions of host addresses. #> "$($int -shr 24 -band 255).$($int -shr 16 -band 255).$($int -shr 8 -band 255).$($int -band 255)" } 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 } } function Resolve-Subnet { <# .SYNOPSIS Resolves an IP/Mask parameter pair (as accepted by Get-Subnet and Get-SubnetHostAddress) into the network's class, mask, addresses, and usable host address range. .NOTES $Mask is deliberately untyped rather than [int] -- callers pass $null to mean "not specified", and an [int] parameter would silently coerce that to 0, indistinguishable from an explicit /0. #> param ( [string]$IP, $Mask ) 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 that callers make against the resolved Mask, for any single-digit mask (/0 - /9). $Mask = [int]$IPandMask[1] } # Validate the IP address and ensure it's IPv4. [ipaddress]::TryParse returns $false for invalid addresses, and also for valid IPv6 addresses. $ParsedIP = $null if (-not [ipaddress]::TryParse($IP, [ref]$ParsedIP) -or $ParsedIP.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { throw "'$IP' is not a valid IPv4 address." } $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 = $ParsedIP $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)) $HostStartAddr = (Convert-IPtoInt64 -ip $NetworkAddr.ipaddresstostring) + 1 $HostEndAddr = (Convert-IPtoInt64 -ip $BroadcastAddr.ipaddresstostring) - 1 [pscustomobject]@{ IPAddr = $IPAddr Mask = $Mask Class = $Class MaskAddr = $MaskAddr NetworkAddr = $NetworkAddr BroadcastAddr = $BroadcastAddr HostStartAddr = $HostStartAddr HostEndAddr = $HostEndAddr } } |