Skip to content

Instantly share code, notes, and snippets.

@JohnLBevan
Last active March 8, 2024 15:34
Show Gist options
  • Save JohnLBevan/a6c819ba5d825f29d465fb0433b54082 to your computer and use it in GitHub Desktop.
Save JohnLBevan/a6c819ba5d825f29d465fb0433b54082 to your computer and use it in GitHub Desktop.
Converts a CIDR notation IP Range to an array of those IPs
<#
.SYNOPSIS
Converts a CIDR notation IP Range to an array of those IPs, or an object containing the first and last values in the range.
All representations of IPs are returned as type: [System.Net.IPAddress]
.DESCRIPTION
Converts a CIDR notation IP Range to an array of those IPs, or an object containing the first and last values in the range.
All representations of IPs are returned as type: [System.Net.IPAddress]
.PARAMETER IpRangeCidr
Gives the range(s) to be converted using CIDR notation (e.g. '127.0.0.0/28')
.PARAMETER AsObject
If specified, instead of returing an array of all values, returns an object with properties FirstIp and LastIp.
.INPUTS
IpRangeCidr may be fed in from the pipeline.
.OUTPUTS
An array of type [System.Net.IPAddress]
Or if AsObject is specified, a PSCustomObject with properties FirstIp and LastIp
.EXAMPLE
PS> Covert-CidrToIpV4List -IpRangeCidr '127.0.0.1/28' | % IPAddressToString
127.0.0.0
127.0.0.1
127.0.0.2
127.0.0.3
127.0.0.4
127.0.0.5
127.0.0.6
127.0.0.7
127.0.0.8
127.0.0.9
127.0.0.10
127.0.0.11
127.0.0.12
127.0.0.13
127.0.0.14
127.0.0.15
.EXAMPLE
PS> Covert-CidrToIpV4List -IpRangeCidr '127.0.0.1/28' -AsObject | Format-Table
FirstIp LastIp
------- ------
127.0.0.0 127.0.0.15
.EXAMPLE
PS> @('127.0.0.0/29', '128.0.0.1/32') | Covert-CidrToIpV4List | % IPAddressToString
127.0.0.0
127.0.0.1
127.0.0.2
127.0.0.3
127.0.0.4
127.0.0.5
127.0.0.6
127.0.0.7
128.0.0.1
.LINK
See https://gist.github.com/JohnLBevan/a6c819ba5d825f29d465fb0433b54082
#>
Function Covert-CidrToIpV4List {
[CmdletBinding()]
Param (
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$IpRangeCidr
,
# by default the cmdlet returns all IPs in the range. Use this switch to just return an object with properties FirstIp and LastIp
[Parameter()]
[Switch]$AsObject
)
Begin {
[string]$ipStr = [string]::Empty
[string]$slashStr = [string]::Empty
}
Process {
foreach ($cidr in $IpRangeCidr) {
$ipStr, $slashStr = $cidr -split '/'
[UInt16]$slash = [UInt16]::Parse($slashStr)
[UInt32]$size = [Math]::Pow(2, 32-$slash) - 1
[UInt32]$mask = [UInt32]::MaxValue - $size
[System.Net.IPAddress]$ip = [System.Net.IPAddress]$ipStr
[UInt32]$firstIp = [System.Net.IPAddress]::Parse($ip.Address).Address -band $mask # note: parse reverses the byte order of the ip / is the same as `$bytes = $ip.GetAddressBytes(); [Array]::Reverse($bytes); [System.Net.IPAddress]::new($bytes -join '.')`
if ($AsObject.IsPresent) {
([PSCustomObject][Ordered]@{
FirstIp = [System.Net.IPAddress]::Parse($firstIp)
LastIp = [System.Net.IPAddress]::Parse($firstIp + $size)
})
} else {
foreach ($i in @(0..$size)) {
[System.Net.IPAddress]::Parse($firstIp + $i)
}
}
}
}
}
#
# If your aim is to check if an IP is in a given range, this function is more efficient
# than using Covert-CidrToIpV4List, as it simply converts IPs to UINTs, using simple
# mathematic operations, so is much faster. It allows you to work with invalid CIDRs
# e.g. `127.0.0.1/31` (which should be `127.0.0.0/31` for a /31 range including `127.0.0.1`)
# so long as they're a valid CIDR format.
#
Function Test-IpInCidr {
[CmdletBinding(DefaultParameterSetName = 'IpsInPipeline')]
Param (
[Parameter(ParameterSetName = 'IpsInPipeline', Mandatory, ValueFromPipeline)]
[Parameter(ParameterSetName = 'CidrsInPipeline', Mandatory)]
[string[]]$IPv4
,
[Parameter(ParameterSetName = 'IpsInPipeline', Mandatory)]
[Parameter(ParameterSetName = 'CidrsInPipeline', Mandatory, ValueFromPipeline)]
[string[]]$IPv4Cidr
,
[Parameter()]
[Switch]$BoolResponse
)
Begin {
[PSCustomObject[]]$cidrList = @()
[PSCustomObject[]]$ipList = @()
switch -Wildcard ($PSCmdlet.ParameterSetName) {
'IpsInPipeline*' {$cidrList = $IPv4Cidr | Convert-Ipv4CidrToUInt}
'CidrsInPipeline*' {$ipList = $IPv4 | ForEach-Object{"$_/32"} | Convert-Ipv4CidrToUInt}
default {throw "ParameterSetName [$_] Not Expected"}
}
}
Process {
switch -Wildcard ($PSCmdlet.ParameterSetName) {
'IpsInPipeline*' {$ipList = $IPv4 | ForEach-Object{"$_/32"} | Convert-Ipv4CidrToUInt}
'CidrsInPipeline*' {$cidrList = $IPv4Cidr | Convert-Ipv4CidrToUInt}
default {throw "ParameterSetName [$_] Not Expected"}
}
foreach ($ip in $ipList) {
foreach ($cidr in $cidrList) {
# note: IP First and Last will be identical; but I've used both here so that if we tweaked this to support CIDR comparisons it would capture any (partially or fully) overlapping CIDRs
if (($ip.Last -ge $cidr.First) -and ($ip.First -le $cidr.Last)) {
if ($BoolResponse.IsPresent) {
return $true
}
([PSCustomObject]@{
IPv4 = $ip.CIDR -replace '/32$', ''
IPv4Cidr = $cidr.CIDR
InRange = $true
})
} else {
if (-not ($BoolResponse.IsPresent)) {
([PSCustomObject]@{
IPv4 = $ip.CIDR -replace '/32$', ''
IPv4Cidr = $cidr.CIDR
InRange = $false
})
}
}
}
if ($BoolResponse.IsPresent) {
return $false
}
}
}
}
Function Convert-Ipv4CidrToUInt {
[CmdletBinding()]
Param (
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$IPv4Cidr
)
Process {
foreach ($ip in $IPv4Cidr) {
if ($ip -match '^(\d+)\.(\d+)\.(\d+)\.(\d+)/(\d+)$') {
[uint]$first = ( `
[uint]::Parse($matches[1]) * 16777216 +
[uint]::Parse($matches[2]) * 65536 +
[uint]::Parse($matches[3]) * 256 +
[uint]::Parse($matches[4])
)
[uint]$last = [Math]::Pow(2, 32-[uint]::Parse($matches[5])) - 1 + $first
([PSCustomObject]@{
CIDR = $ip
First = $first
Last = $last
})
} else {
throw "Invalid IPv4 CIDR [$ip]"
}
}
}
}
# Example Use Case
# This will return the first matching CIDR for the given IP; if no CIDR matches it will return nothing
$polishGeoCidrs = Invoke-RestMethod -Uri 'https://www.ipdeny.com/ipblocks/data/aggregated/pl-aggregated.zone' -Method 'Get'
$zigUksGeoPoland | Test-IpInCidr -IPv4 '213.241.0.5' | ? InRange | Select -First 1
# note: you can use either CIDRs or IPs as your pipeline input, or neither, passing both as arrays to parameters.
# in the abvoe use case I used the pipeline so we'd terminate on first match whilst getting the efficiency gain of not
# parsing every CIDR if we matched early.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment