Background

Writing some Desired State Configuration I had to programmatically create some IP-allow rules for a service with a default deny rule.

I wanted to create a rule to permit devices on a local subnet to access a service, based on a given hosts IP address. Investigating the Get-NetIPAddress cmdlet, I noted that it returns a hosts allocated IP address and, rather than a subnet mask in octet notation, the network CIDR or PrefixLength1.

For example:

IPv4Address: 172.16.1.62
PrefixLength: 21

For the IP allow rule, I needed something like:

Network: 172.16.0.0
SubnetMask: 255.255.248.0

I solved this problem using some of the bitwise operators in PowerShell. I wrote what I learned into a function to share here…

Code

function Get-NetworkIPv4 {
    param(
        [string]$ipAddress,
        [int]$cidr
    )
    $parsedIpAddress = [System.Net.IPAddress]::Parse($ipAddress)
    $shift = 64 - $cidr
    
    [System.Net.IPAddress]$subnet = 0

    if ($cidr -ne 0) {
        $subnet = [System.Net.IPAddress]::HostToNetworkOrder([int64]::MaxValue -shl $shift)
    }

    [System.Net.IPAddress]$network = $parsedIpAddress.Address -band $subnet.Address

    return [PSCustomObject]@{
        Network = $network
        SubnetMask = $subnet
    }
}

Example Usage

PS E:\> Get-NetworkIPv4 -ipAddress '172.16.1.62' -cidr 21

Network    SubnetMask
-------    ----------
172.16.0.0 255.255.248.0

Explanation

The .NET System.Net.IPAddress class is used initially to parse the supplied IP address. It will take a valid IP address string and store it as the appropriate underlying type.

We also use it again for storing the resultant $network and $subnet as it will implicitly ToString() the stored values when required.

Network IPv4 addresses and their associated subnet masks are 32 bits in length.

A bit is either 0 or 1. A subnet mask representing a single host, is either 255.255.255.255 in octet notation or /32 in CIDR notation.

In binary it could be written out as:

11111111 11111111 11111111 11111111

A subnet mask used to identify a network with a maximum of 254 possible IP addresses is either 255.255.255.0 in octet notation or /24 in CIDR notation.

In binary, this would be:

11111111 11111111 11111111 00000000

Notice the difference? In the first example, all 32 bits are set to 1. In the second example, the first 24 bits are set to one and the remaining 8 bits are set to 0.

The difference between these two binary values, is that the second one has had all its bits shifted 8 registers to the left.

i = 11111111 11111111 11111111 11111111 
i = i << 8 
print i
'11111111 11111111 11111111 00000000'

This how the function above works, save for the fact that System.Net.IPAddress uses signed integers (the most significant bit is negative). That means we have to use int64 (signed) because int32 (also signed) does not have enough registers to handle a positive 32-bit value. Also, instead of starting with the number 32 and and subtracting the supplied $cidr from it in order to calculate the number of bits the [int64]::MaxValue is shifted by, the function starts with 64 and subtacts the supplied $cidr from that.

Once the function has worked out how many bits to shift [int64]::MaxValue by, it gets on and does the shift operation using the PowerShell bitwise shift left (-shl) operator.

$subnet = [System.Net.IPAddress]::HostToNetworkOrder([int64]::MaxValue -shl $shift)

At this point I ran into a byte ordering issue - When setting the $subnet to the resultant bit shifted [Int64]::MaxValue, I was getting a network mask that was in the wrong order. To fix this, I used the HostToNetworkOrder method to swap the byte order. Or in otherwords, convert the value from little-endian to big-endian.

Now we have a representation of the subnet, we can use the -band (binary and) operator with the IP Address and the subnet to cancel out all of the bits in the hosts IP address which do not coincide with bits in the subnet mask2.

[System.Net.IPAddress]$network = $parsedIpAddress.Address -band $subnet.Address

The final step is returning the $network and $subnet values to the functions caller.

Notes

This code sample:

  • does not do any input validation
  • is not designed to work with IPv6

Conclusion

I often find that problems almost solve themelves once the data concerned is converted into the right format and marshalled into the right structure.

Learning to convert data from one format to another in simple cases like this will no doubt help anyone face more complex problems in the future.

References

  1. Wikipedia - IPv4
  2. docs.microsoft.com - System.Net.IPAddress
  3. docs.microsoft.com - Windows PowerShell Reference

  1. This is no doubt to provide an API that is both IPv4 and IPv6 compatible ↩︎

  2. This is probably worthy of more explanation. Binary or bitwise operators maybe a future topic. ↩︎