Public/network/Get-SubnetInfo.ps1
|
#Requires -Version 5.1 function Get-SubnetInfo { <# .SYNOPSIS Calculates subnet information from an IP address and subnet mask or CIDR notation. .DESCRIPTION Computes detailed subnet properties including network address, broadcast address, first/last usable host, total host count, and wildcard mask. Accepts input in CIDR notation (e.g., 192.168.1.0/24) or as separate IP and mask parameters. This is a pure calculation function with no network access required. .PARAMETER IPAddress IP address in standard dotted notation (e.g., 192.168.1.100) or CIDR notation (e.g., 192.168.1.0/24). Accepts pipeline input. .PARAMETER PrefixLength Subnet prefix length (CIDR notation). Valid range: 0-32. Not required if IPAddress includes CIDR notation. .PARAMETER SubnetMask Subnet mask in dotted notation (e.g., 255.255.255.0). Alternative to PrefixLength. .EXAMPLE Get-SubnetInfo -IPAddress '192.168.1.0/24' Calculates subnet info for a /24 network using CIDR notation. .EXAMPLE Get-SubnetInfo -IPAddress '10.0.0.50' -PrefixLength 16 Calculates subnet info for a /16 network. .EXAMPLE Get-SubnetInfo -IPAddress '172.16.0.0' -SubnetMask '255.255.240.0' Uses traditional subnet mask notation. .EXAMPLE '192.168.1.0/24', '10.0.0.0/8', '172.16.0.0/12' | Get-SubnetInfo Calculates info for multiple subnets via pipeline. .OUTPUTS PSWinOps.SubnetInfo .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-03-21 Requires: PowerShell 5.1+ / Windows only Permissions: No admin required (pure calculation, no network access) #> [CmdletBinding(DefaultParameterSetName = 'CIDR')] [OutputType('PSWinOps.SubnetInfo')] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string[]]$IPAddress, [Parameter(Mandatory = $false, ParameterSetName = 'CIDR')] [ValidateRange(0, 32)] [int]$PrefixLength, [Parameter(Mandatory = $false, ParameterSetName = 'Mask')] [ValidateScript({ try { $null = [System.Net.IPAddress]::Parse($_) $true } catch { throw "Invalid subnet mask format: '$_'" } })] [string]$SubnetMask ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting subnet calculation" function ConvertTo-UInt32 { param([System.Net.IPAddress]$IP) $bytes = $IP.GetAddressBytes() [uint32](([uint32]$bytes[0] -shl 24) -bor ([uint32]$bytes[1] -shl 16) -bor ([uint32]$bytes[2] -shl 8) -bor [uint32]$bytes[3]) } function ConvertFrom-UInt32 { param([uint32]$Value) [System.Net.IPAddress]::new([byte[]]@( [byte](($Value -shr 24) -band 0xFF), [byte](($Value -shr 16) -band 0xFF), [byte](($Value -shr 8) -band 0xFF), [byte]($Value -band 0xFF) )) } function ConvertTo-PrefixLength { param([System.Net.IPAddress]$Mask) $maskInt = ConvertTo-UInt32 -IP $Mask $bits = 0 $current = $maskInt while ($current -band 0x80000000) { $bits++ $current = ($current -shl 1) -band 0xFFFFFFFF } return $bits } } process { foreach ($ip in $IPAddress) { try { # Parse CIDR notation if present $parsedIP = $null $parsedPrefix = 0 if ($ip -match '^(.+)/([0-9]+)$') { $parsedIP = [System.Net.IPAddress]::Parse($Matches[1]) $parsedPrefix = [int]$Matches[2] if ($parsedPrefix -lt 0 -or $parsedPrefix -gt 32) { Write-Error "[$($MyInvocation.MyCommand)] Invalid prefix length: $parsedPrefix" continue } } elseif ($PSBoundParameters.ContainsKey('PrefixLength')) { $parsedIP = [System.Net.IPAddress]::Parse($ip) $parsedPrefix = $PrefixLength } elseif ($PSBoundParameters.ContainsKey('SubnetMask')) { $parsedIP = [System.Net.IPAddress]::Parse($ip) $maskIP = [System.Net.IPAddress]::Parse($SubnetMask) $parsedPrefix = ConvertTo-PrefixLength -Mask $maskIP } else { Write-Error "[$($MyInvocation.MyCommand)] Specify prefix length via CIDR notation (/24), -PrefixLength, or -SubnetMask for '$ip'" continue } # Core calculations $ipInt = ConvertTo-UInt32 -IP $parsedIP # Build mask from prefix if ($parsedPrefix -eq 0) { [uint32]$maskInt = 0 } else { [uint32]$maskInt = [uint32]::MaxValue -shl (32 - $parsedPrefix) } [uint32]$wildcardInt = $maskInt -bxor [uint32]::MaxValue [uint32]$networkInt = $ipInt -band $maskInt [uint32]$broadcastInt = $networkInt -bor $wildcardInt # Usable hosts if ($parsedPrefix -ge 31) { # /31 = point-to-point (RFC 3021), /32 = single host $firstHostInt = $networkInt $lastHostInt = $broadcastInt $totalHosts = if ($parsedPrefix -eq 32) { 1 } else { 2 } $usableHosts = $totalHosts } else { $firstHostInt = $networkInt + 1 $lastHostInt = $broadcastInt - 1 $totalHosts = [math]::Pow(2, (32 - $parsedPrefix)) $usableHosts = $totalHosts - 2 } [PSCustomObject]@{ PSTypeName = 'PSWinOps.SubnetInfo' IPAddress = $parsedIP.ToString() PrefixLength = $parsedPrefix SubnetMask = (ConvertFrom-UInt32 -Value $maskInt).ToString() WildcardMask = (ConvertFrom-UInt32 -Value $wildcardInt).ToString() NetworkAddress = (ConvertFrom-UInt32 -Value $networkInt).ToString() BroadcastAddress = (ConvertFrom-UInt32 -Value $broadcastInt).ToString() FirstUsableHost = (ConvertFrom-UInt32 -Value $firstHostInt).ToString() LastUsableHost = (ConvertFrom-UInt32 -Value $lastHostInt).ToString() TotalHosts = [long]$totalHosts UsableHosts = [long]$usableHosts CIDR = "{0}/{1}" -f (ConvertFrom-UInt32 -Value $networkInt).ToString(), $parsedPrefix Timestamp = Get-Date -Format 'o' } } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed to calculate subnet info for '$ip': $_" } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed subnet calculation" } } |