In the process of working on some contributions to Salt (a fantastic new infrastructure automation system), I needed to figure out how to find the CIDR notation for a subnet, given only an IP address and netmask. There is a Python module called netaddr which is capable of performing this calculation, but it is not a core Python module and using it would have required this module to be added to Salt's dependency list. So, I needed to find a good way of calculating this information without the aid of a Python module. To my surprise, documentation on how to do this doesn't appear to be that common on the web, so I thought I'd share it.

But first, a little bit of explanation for those that may need it. An IPv4 address contains 4 octets, each ranging from 0 to 255 (for example, 56.78.123.45). However, this notation is really just shorthand, designed be easier for humans to read. An IP address is really just a a 32-bit number, with bits 1-8 making up the first octet, 9-16 making up the second, etc. In binary, 255 is 11111111, while 0 is 00000000. The network mask (sometimes also referred to as a "netmask") uses the same notation as an IP address. When converted from that notation to binary, the number of zeros at the end will let you know the size of the network. A network mask (in binary) will be several 1s followed by (in most cases) several 0s. In the case of 255.255.255.0, there are 24 1s followed by 8 0s:

Normal: 255      255      255      0
Binary: 11111111 11111111 11111111 00000000

CIDR notation for a network would be the starting IP of the network followed by /NN, where NN is equal to 32 - the number of zeros. Since there are 8 zeros at the end, a netmask of 255.255.255.0 would be a /24 network. These are quite common. For the IP address 56.78.123.45, the CIDR notation for the network would be 56.78.123.0/24. /24 networks are easy because they always start at zero, but what if the netmask was 255.255.252.0? How would you find the start of that network?

The answer lies in bitwise operations, specifically the bitwise AND. A bitwise AND will take two binary numbers and compare the first bit of both numbers to each other, then the second bit of each number, etc., and will yield a 1 when both are 1, and 0 if either (or both) are not 1. Remember that a netmask will always be 1s up to a certain point. That point represents the start of the network. So executing a bitwise AND on the IP and netmask will give you 1s where there were 1s in the IP address, but only up to the point where the network began. The rest of the bits in the result will all be zeros.

56.78.123.45  = 00111000 01001110 01111011 00101101
255.255.252.0 = 11111111 11111111 11111100 00000000
---------------------------------------------------
                00111000 01001110 01111000 00000000 = 56.78.120.0

As you can see, the result is 56.78.120.0. Because there are 10 zeros at the end of the netmask, this is a /22 (32 - 10) network, making the CIDR notation for this network 56.78.120.0/22.

Of course, it's not too fun to do these conversions every time you want to get this information. There is a command-line tool called ipcalc which does this for you quite easily (The link was for a web version, but you can get a command-line version of the same tool in most Linux distributions).

$ ipcalc -b 56.78.123.45/255.255.252.0 | grep Network
Network:   56.78.120.0/22

Let's make a programming exercise out of this, though. In Python, the bitwise AND operator is a single ampersand.

>>> 56 & 255
56
>>> 123 & 252
120

So, the below Python code will give you the CIDR notation:

#!/usr/bin/python2

import sys
from socket import inet_aton

USAGE = 'usage: {0} ipaddr netmask\n'.format(sys.argv[0])

def get_net_size(netmask):
    binary_str = ''
    for octet in netmask:
        binary_str += bin(int(octet))[2:].zfill(8)
    return str(len(binary_str.rstrip('0')))

if len(sys.argv) != 3:
    sys.stderr.write(USAGE)
    sys.exit(1)

# validate input
try:
    inet_aton(sys.argv[1])
    inet_aton(sys.argv[2])
except:
    sys.stderr.write('IP address or netmask invalid\n')
    sys.stderr.write(USAGE)
    sys.exit(2)

ipaddr = sys.argv[1].split('.')
netmask = sys.argv[2].split('.')

# calculate network start
net_start = [str(int(ipaddr[x]) & int(netmask[x]))
             for x in range(0,4)]

# print CIDR notation
print '.'.join(net_start) + '/' + get_net_size(netmask)

And here's the output:

$ python2 cidr_notation.py 12.34.56.78 255.255.255.248
12.34.56.72/29

This is a /29 network, which has 2(32-29), or 23, or 8 IP addresses, so it would span the IP addresses 12.34.56.72 to 12.34.56.79.

Hopefully you grok CIDR a little more now than you did a few minutes ago. :)