Sunday, January 29, 2012

Amusement: Python and Netmasks

As a network engineer, it's not uncommon for me to need to convert between hex and decimal. While I'm reasonably good at doing this in my head for smaller numbers, when it comes to deciphering stuff like higher TCP or UDP port numbers written in hex, I usually end up using the Python interpreter that's usually open somewhere on my machine. For me, the Python interpreter is the best general purpose calculator app I've found. Using the port number for Flash as an example:

jswan$ python
Python 2.7.2 (v2.7.2:8527427914a2, Jun 11 2011, 15:22:34)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 0x78f
1935
>>> hex(1935)
'0x78f'

The topic of numeric base conversions often makes my mind drift to every former networking instructor's pet topics, IPv4 subnetting. The other day, I started playing with using the Python interpreter to find network IDs, which is really easy as long as you're using hex:

>>> hex(0x0a0a0a25 & 0xffffffe0)
'0xa0a0a20'

This little snippet finds the bitwise-AND of 10.10.10.37 and 255.255.255.224, which as every up-and-coming CCNA knows is the network ID for that subnet: 10.10.10.32. Back when I was teaching Cisco classes full-time, I used to have a never-ending argument with another instructor about the fact that I didn't teach bitwise operations as part of subnetting: my position was that if all you are doing is solving subnet problems with your squishy human brain, you don't need to learn a bunch of truth tables when there are easier ways to do it in human memory. However, if you're writing code you actually do need to do bitwise operations.

Unfortunately most of us (me included) aren't wired for reading IPv4 addresses in hex. So I started wondering how little Python code I could use to calculate network IDs for IPv4 in dotted decimal. A little screwing around and I started wondering if I could fit the entire thing into a Twitter post. Here's what I came up with:

while 1:'.'.join([str(a&m)for a,m in zip([int(n)for n in raw_input('addr?').split('.')],[int(n)for n in raw_input('mask?').split('.')])])

 Try it:

addr?1.1.1.37
mask?255.255.255.224
'1.1.1.32'

Now I realize this bit of code is neither particularly clever nor easy to read, which makes it bad code. But hey, it fits in a single tweet. It works by using one of Python's coolest features, the list comprehension. If we start with the innermost parts, it makes more sense:

[int(n) for n in raw_input('addr?').split('.')]
[int(n) for n in raw_input('addr?').split('.')]

These two sections return lists of integers corresponding to the address and mask the user enters. For example:

[1,1,1,37]
[255,255,255,224]

Next, the "zip" function returns a list of tuples that pair corresponding entries in the two lists:

[(1, 255), (1, 255), (1, 255), (37, 224)]

Next, the outermost list comprehension performs a bitwise-AND of each tuple, returning the octets of our network ID in a new list:

[1,1,1,32]

Finally, the "join" method puts them back together into "1.1.1.32", and "while 1:" makes it a loop until you ctrl-c out of it.

Working with netmasks in CIDR notation is a bit more complicated, and requires more than one line of code--I'll save that for another post.