Tuesday, February 8, 2011

Episode #133: Name's the Same?

Hal's on mailroom duty

This week's challenge comes to us courtesy of Brian Finn:

We are building a new external DNS server for our network, and I was trying to figure out a way to make sure I had the new one giving out the same answers as the old one. I was going to throw a bunch of host names at both servers and make sure they handed out the same IP addresses. I figured out how to do it with dig on Linux:

dig -f hostnames.txt +noall +answer @dnsserver1 > output1
dig -f hostnames.txt +noall +answer @dnsserver2 > output2
diff output1 output2

Is there a one-liner that can do this? Can it even be done in Windows?

I'll leave the Windows answer to my esteemed colleague, but the one-liner answer should be familiar to regular readers of the blog:

diff <(dig -f hostnames.txt +noall +answer @dnsserver1) \
<(dig -f hostnames.txt +noall +answer @dnsserver2)

The solution is to use bash's "<(...)" syntax to substitute the command output in place of the file name arguments in the diff command line.

I did want to compliment Brian on his excellent dig fu here. Normally I use the "host" command when doing DNS queries from the command-line because the output is so much easier to deal with. But in this case because we want to query an entire list of hostnames, the "-f" option to dig comes in really handy. To simplify the dig output, Brian first sets the "+noall" option which would normally suppress all output from the command. But he follows that up with "+answer", which means to only output the answers returned by the remote name server. So you get nice clean output like this:

$ dig -f hostnames.txt +noall +answer @127.0.0.1
www.deer-run.com. 3600 IN CNAME newwinkle.deer-run.com.
newwinkle.deer-run.com. 3600 IN A 67.18.149.10
test.deer-run.com. 3600 IN A 192.168.168.168
$ dig -f hostnames.txt +noall +answer @67.18.149.10
www.deer-run.com. 14400 IN CNAME newwinkle.deer-run.com.
newwinkle.deer-run.com. 14400 IN A 67.18.149.10
test.deer-run.com. 14400 IN CNAME newwinkle.deer-run.com.
newwinkle.deer-run.com. 14400 IN A 67.18.149.10

You can see from the above output that I've set up a test case using a fake DNS server on my local box and the master server for my domain. The records for www.deer-run.com are the same, but test.deer-run.com differs between the two servers. So what's going to happen when I try my one-liner here?

$ diff <(dig -f hostnames.txt +noall +answer @127.0.0.1) \
<(dig -f hostnames.txt +noall +answer @67.18.149.10)

1,3c1,4
< www.deer-run.com. 3600 IN CNAME newwinkle.deer-run.com.
< newwinkle.deer-run.com. 3600 IN A 67.18.149.10
< test.deer-run.com. 3600 IN A 192.168.168.168
---
> www.deer-run.com. 14400 IN CNAME newwinkle.deer-run.com.
> newwinkle.deer-run.com. 14400 IN A 67.18.149.10
> test.deer-run.com. 14400 IN CNAME newwinkle.deer-run.com.
> newwinkle.deer-run.com. 14400 IN A 67.18.149.10

Why is diff reporting that all of the output is different between the two commands? Because if you look carefully at the second column of output you'll see that the time-to-live values returned by the two servers are different. That's because I set the default TTL value differently in the two DNS installations.

So the question is, is this a significant difference that deserves to be reported on? Some folks might think that it was, and so the output above is correct. However, if you don't care about differing TTL values, then you'll probably want to do something like this:

$ diff <(dig -f hostnames.txt +noall +answer @127.0.0.1 | awk '{print $1,$4,$5}') \
<(dig -f hostnames.txt +noall +answer @67.18.149.10 | awk '{print $1,$4,$5}')

3c3,4
< test.deer-run.com. A 192.168.168.168
---
> test.deer-run.com. CNAME newwinkle.deer-run.com.
> newwinkle.deer-run.com. A 67.18.149.10

Kind of long for a "one-liner", but it gets the job done.

Well Brian basically did my work for me this week. Let's see how Tim's handling things on his solo mission.

Tim celebrates a Packers win!

I'm a big Packers fan and I'm excited they won on Sunday. Congratulations to the Pack and Go Pack Go! But now that football is over, we'll get back to some Windows fu.

Unfortunately, there isn't a nice PowerShell cmdlet to get the results from DNS. We have to use the old nslookup command, which doesn't give us nice output objects. In addition, nslookup sends some of its messages to stderr, so we'll strip that from the output by sending it to the $null garbage can. Also, we'll set the timeout value to make sure we don't miss any lookups and mess up our output. Here is the command and its output.

PS C:\> nslookup -timeout=20 www.commandlinekungfu.com 8.8.8.8 2>$null
Server: google-public-dns-a.google.com
Address: 8.8.8.8

Name: www.commandlinekungfu.com
Address: 209.20.73.195


Our problem is the results show the name and IP address of name server we are querying. Since we don't have a built-in cmdlet to just give us answers, we will have to strip the first three lines from the output. We can do just that by using the range operator to only access the lines of output we want.

PS C:\> (nslookup -timeout=20 www.commandlinekungfu.com 8.8.8.8 2>$null )[3..99]
Name: www.commandlinekungfu.com
Address: 209.20.73.195


This command returns the 3rd through 99th line of output. Of course we don't have 99 lines of output, but it makes sure that we get all the output. The output of each command can be fed into the Compare-Object cmdlet to see if the DNS servers are returning different results.

PS C:\> Compare-Object `
(nslookup -timeout=20 www.commandlinekungfu.com 8.8.8.8 2>$null )[3..99]
(nslookup -timeout=20 www.commandlinekungfu.com 8.8.4.4 2>$null )[3..99]


<No Output>


No output means everything matches. Great!

What does it look like if the output is different?

PS C:\> Compare-Object `
(nslookup -timeout=20 www.commandlinekungfu.com 8.8.8.8 2>$null )[3..99]
(nslookup -timeout=20 www.commandlinekungfu.com 127.0.0.1 2>$null )[3..99]


InputObject SideIndicator
----------- ------------
Address: 209.20.73.195 <=
Address: 111.22.33.222 =>


That works for manually checking each hostname, but what if we have a text file containing all the DNS names we want to check? Our first problem is the output above does not show the name we are looking up so we would end up with a list of IP addresses without the context of the hostname. We have to change the output so the name and address is on one line.

PS C:\> [string]::Join(" ", (nslookup -timeout=20 www.commandlinekungfu.com 8.8.8.8 2>$null )[3..99])
Name: www.commandlinekungfu.com Address: 209.20.73.195


This command removes each line break and replaces it with a space. We then read the file and and check each hostname inside the ForEach-Object cmdlet (alias %).

PS C:\> Get-Content hostnames.txt | % { Compare-Object
[string]::Join(" ", (nslookup -timeout=20 $_ 8.8.8.8 2>$null )[3..99])
[string]::Join(" ", (nslookup -timeout=20 $_ 127.0.0.1 2>$null )[3..99])
}


InputObject SideIndicator
----------- ------------
Name: www.commandlinekungfu.com Address: 209.20.73.195 <=
Name: www.commandlinekungfu.com Address: 111.22.33.222 =>


While definitely not as simple and elegant as Hal's solution, it does work.

By the way, there are a number of plug-ins that emmulate dig and give us pretty objects, my favorite is over here.