internal/Test-IpAddressInSubnet.ps1

<#
.SYNOPSIS
    Determine if an IP address exists in the specified subnet.
.EXAMPLE
    PS C:\>Test-IpAddressInSubnet 192.168.1.10 -Subnet '192.168.1.1/32','192.168.1.0/24'
    Determine if the IPv4 address exists in the specified subnet.
.EXAMPLE
    PS C:\>Test-IpAddressInSubnet 2001:db8:1234::1 -Subnet '2001:db8:a::123/64','2001:db8:1234::/48'
    Determine if the IPv6 address exists in the specified subnet.
.INPUTS
    System.Net.IPAddress
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Test-IpAddressInSubnet {
    [CmdletBinding()]
    [OutputType([bool], [string[]])]
    param (
        # IP Address to test against provided subnets.
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1)]
        [ipaddress[]] $IpAddresses,
        # List of subnets in CIDR notation. For example, "192.168.1.0/24" or "2001:db8:1234::/48".
        [Parameter(Mandatory = $true)]
        [string[]] $Subnets,
        # Return list of matching subnets rather than a boolean result.
        [Parameter(Mandatory = $false)]
        [switch] $ReturnMatchingSubnets
    )

    begin {
        function ConvertBitArrayToByteArray([System.Collections.BitArray] $BitArray) {
            [byte[]] $ByteArray = New-Object byte[] ([System.Math]::Ceiling($BitArray.Length / 8))
            $BitArray.CopyTo($ByteArray, 0)
            return $ByteArray
        }

        function ConvertBitArrayToBigInt([System.Collections.BitArray] $BitArray) {
            return [bigint][byte[]](ConvertBitArrayToByteArray $BitArray)
        }
    }

    process {
        foreach ($IpAddress in $IpAddresses) {
            if ($IpAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
                [int32] $bitIpAddress = [BitConverter]::ToInt32($IpAddress.GetAddressBytes(), 0)
            }
            else {
                [System.Collections.BitArray] $bitIpAddress = $IpAddress.GetAddressBytes()
            }

            [System.Collections.Generic.List[string]] $listSubnets = New-Object System.Collections.Generic.List[string]
            [bool] $Result = $false
            foreach ($Subnet in $Subnets) {
                [string[]] $SubnetComponents = $Subnet.Split('/')
                [ipaddress] $SubnetAddress = $SubnetComponents[0]
                [int] $SubnetMaskLength = $SubnetComponents[1]

                if ($IpAddress.AddressFamily -eq $SubnetAddress.AddressFamily) {
                    if ($IpAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
                        ## Supports IPv4 (32 bit) only but more performant than BitArray?
                        #[int32] $bitIpAddress = [BitConverter]::ToInt32($IpAddress.GetAddressBytes(), 0)
                        [int32] $bitSubnetAddress = [BitConverter]::ToInt32($SubnetAddress.GetAddressBytes(), 0)
                        [int32] $bitSubnetMaskHostOrder = 0
                        if ($SubnetMaskLength -gt 0) {
                            $bitSubnetMaskHostOrder = -1 -shl (32 - $SubnetMaskLength)
                        }
                        [int32] $bitSubnetMask = [ipaddress]::HostToNetworkOrder($bitSubnetMaskHostOrder)

                        ## Check IP
                        if (($bitIpAddress -band $bitSubnetMask) -eq ($bitSubnetAddress -band $bitSubnetMask)) {
                            if ($ReturnMatchingSubnets) {
                                $listSubnets.Add($Subnet)
                            }
                            else {
                                $Result = $true
                                continue
                            }
                        }
                    }
                    else {
                        ## BitArray supports IPv4 (32 bits) and IPv6 (128 bits). Would Int128 type in .NET 7 improve performance?
                        #[System.Collections.BitArray] $bitIpAddress = $IpAddress.GetAddressBytes()
                        [System.Collections.BitArray] $bitSubnetAddress = $SubnetAddress.GetAddressBytes()
                        [System.Collections.BitArray] $bitSubnetMask = New-Object System.Collections.BitArray -ArgumentList ($bitSubnetAddress.Length - $SubnetMaskLength), $true
                        $bitSubnetMask.Length = $bitSubnetAddress.Length
                        [void]$bitSubnetMask.Not()
                        [byte[]] $ByteArray = ConvertBitArrayToByteArray $bitSubnetMask
                        [array]::Reverse($ByteArray)  # Convert to Network byte order
                        [System.Collections.BitArray] $bitSubnetMask = $ByteArray
                
                        ## Check IP
                        if ((ConvertBitArrayToBigInt $bitIpAddress.And($bitSubnetMask)) -eq (ConvertBitArrayToBigInt $bitSubnetAddress.And($bitSubnetMask))) {
                            if ($ReturnMatchingSubnets) {
                                $listSubnets.Add($Subnet)
                            }
                            else {
                                $Result = $true
                                continue
                            }
                        }
                    }
                }
            }

            ## Return list of matches or boolean result
            if ($ReturnMatchingSubnets) {
                if ($listSubnets.Count -gt 1) { Write-Output $listSubnets.ToArray() -NoEnumerate }
                elseif ($listSubnets.Count -eq 1) { Write-Output $listSubnets.ToArray() }
                else {
                    $Exception = New-Object ArgumentException -ArgumentList ('The IP address {0} does not belong to any of the provided subnets.' -f $IpAddress)
                    Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ObjectNotFound) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'TestIpAddressInSubnetNoMatch' -TargetObject $IpAddress
                }
            }
            else {
                Write-Output $Result
            }
        }
    }
}