Tuesday, May 10, 2011

Episode #146: Hard CIDR

Tim is the mailman:

Most of our episode ideas come from the mailbag, and we can never have too many. Usually, we usually have too few, so keep sending in ideas. This episode was inspired by the modern art masterpiece sent in by Matt Graeber. His "piece" reads a file, expands a list of CIDR formatted IP addresses, and does a reverse DNS lookup against each IP address.

C:\> cmd.exe /v:on /c "FOR /F "tokens=1-5 delims=./" %G IN (ips.txt) DO
@(set network=%G.%H.%I.& set /a fourth="%J+1" 1^>nul & set mask=%K&
set /a end="(2 ^<^< (32-!mask!-1))-2+!fourth!" 1>nul &
echo !network!!fourth!-!end!& echo ****************** &
FOR /L %O IN (!fourth!,1,!end!) DO @(for /F "tokens=2" %P in
('nslookup !network!%O server 2^>nul ^| find "Name:"') do
@echo !network!%O %P & ping -n 3>nul))" 2>nul

That is some serious cmd madness. Since it is such beautiful art, we will leave it to the reader to behold and interpret for himself, but we will go over a more simplified version.

It should be noted, that all the commands presented today only work with 24+ bit masks. Using built in tools to expand the CIDR format for a mask less than 24 is significantly more work because we have to manipulate the third (and possibly second or first) octet(s) as well. We'll use the file below as our sample input file:

We'll simplify Matt's command so it only expands and outputs the IP addresses. From there, you can replace the echo command with the command of your choice. This CIDR expansion is very handy for wraping commands that only take a single IP as input.

The simplified version of the CIDR expander is:

C:\> cmd.exe /v:on /c "FOR /F "tokens=1-5 delims=./" %G IN (ips.txt) DO
@(set network=%G.%H.%I.& set /a fourth="%J" 1^>nul & set mask=%K&
set /a end="(2 ^<^< (32-!mask!-1))-1+!fourth!" 1>nul &
FOR /L %O IN (!fourth!,1,!end!) DO @echo !network!%O)" 2>nul

The command begins by enabling delayed variable expansion (explained at the end of Episode #46). The For Loop then reads our file and splits the text on each period and forward slash, so we have variables that hold each octet (%G-%J) as well as the number of bits in the subnet mask (%K). Next, a bit of cmd math is done to calculate the last IP address in the range. Finally, another For loop is used to output all of the IP addresses. Of course, we could do something else with the IP addresses instead of just displaying them, like ping or nslookup.

The math used to calculate the upper bound of our network uses a Logical Shift Left (<<). This operator shifts each bit to the left one location. So 2 (00000010) left shifted by 1 is 4 (00000100) and 2 left shifted twice is 8 (000010000). This operator is used to replace the nonexistent 2^x command.


The PowerShell command to expand the addresses ranges is:

PS C:\> gc ips.txt | % {
$a[3]..($a[3]-1+[Math]::Pow(2,32-$a[4])) | % {
echo ($a[0],$a[1],$a[2],$_ -join ".")

We start by reading our file using the Get-Content cmdlet (aliases gc and cat). Each line is piped into a ForEach-Object loop to be parsed and manipulated.

Inside our loop we split each string into an array of substrings, using period and forward slash as delimiters. We then end up with $a[0] through $a[3] holding the first through fourth octets and $a[5] is the mask.

The range operator (..) is use to generate a list of numbers between our lower bound (last octet) and the upper bound (end of network range). Again, we use a little math to determine the size of the range. In short, the size of the range is 2^(32-masklen). There isn't a native exponent operator, so we use the built in .NET Math class to do our power function.

The output of the range is fed into another ForEach-Object cmdlet. Inside this cmdlet's scriptblock is where the IP address is reassembled and output.

Now that we have the output, we can easily feed it to another command. For example, say we wanted to ping each of the IP addresses:

 PS C:\> ... | % { ping $_ }

No off to go find some hard cider to make this pain go away...

Hal is the eggman

Tim and I usually work up our pieces of the blog independently and then jam them together using a few insults as connective tissue. It's interesting that in this case my thought process developed along a parallel course with Tim's transition from CMD.EXE to Powershell.

My initial attempt looked a lot like Matt and Tim's CMD.EXE solutions:

while read line; do 
for ((i=0; $i < $(( 2 ** (32-$exp) )); i++)); do
oct=$(($lastoct + $i));
echo $net.$oct;
done <input

The outer while loop reads the input file line-by-line. Then I exploit the bash variable substitution operator ("${var/pattern/sub}") to pull the address and exponent apart, and to strip the last octet from the address. After that it's just a for loop to create the list of addresses. That little "echo" statement in the middle of the inner loop is the whole purpose of the exercise-- the rest is just grisly parsing and setup.

So I wasn't really happy with my solution. But then it occurred to me that I don't have to do the parsing. The shell will do it for me!

while read o1 o2 o3 o4 exp; do
for ((i=o4; $i < $(( $o4 + 2 ** (32-$exp) )); i++)); do
echo $o1.$o2.$o3.$i;
done <input

The trick is to set IFS so that the shell automatically tokenizes each line as we "read" it in the while loop (essentially we're accomplishing the same thing that Tim does with his Powershell split() operator). If IFS is set to "./" then each octet and the exponent is broken out into a different variable and I don't have to do any parsing myself. This is a very useful technique when you're dealing with any "strongly delimited" input-- like /etc/passwd, /etc/shadow, and so on.

You'll also notice that I cleaned up my loop control a little bit so that I save myself a "$((...))" operation inside the inner loop. Rather than having the loop control variable run from 0..2**(32-exp) and then adding the value to the last octet inside the loop, it's easier to start the loop with the last octet value and increment from there.

By the way, some of you may be wondering why I didn't use the built-in range operator in bash and make my for loop be something like:

for $i in {$o4..$(($o4 + 2**(32-$exp) - 1))} ...

I actually tried this approach first. But it turns out that bash only lets you use literal values-- integers or characters-- in a "{a..z}" operation. This actually strikes me as something of a bug.

In any event, that's why I ended up opting for the C-style for loop instead. I could have used something like "$( seq $o4 $(($o4 + 2**(32-$exp) - 1)) )", but why call an external program when you don't have to?

So goo goo g'joob and have a great week everybody!