Home > powershell > Revisiting DNS testing: Checking SOA serial matching across servers

Revisiting DNS testing: Checking SOA serial matching across servers

What to find in this updated script

We will add ‘SOA serial value checking’ to our DNS testing Powershell script. This new version of the script shows you how to do advanced DNS queries with Powerscript. It’s also an example how to reuse .NET assemblies available in EXE format.

The story behind this update

Just last week I’ve spent the better part of a day trying to find out why one of the domains hosted by us had name resolution problems. To make things more complicated, these problems did depend on the actual internet provider (read: name server) of the clients. After a long debugging session and kind help from two other institutions which are hosting secondary zones for us on their name servers, I finally was able to ironing out the problems with our DNS configuration. And like it is quite often the case, I have to blame myself for causing these problems. But the  root cause of these problems is not the issue for this post. Instead it prompted me to augment my DNS testing Powershell script. I’ve now added code to enable checking whether the ‘SOA serial’ is the same for all name servers hosting a zone. This test simply shows whether all name servers for your zone are ‘up to date’. If one or more of the servers hosting a secondary copy of your zone are lagging behind, you probably have a zone transfer problem that you might want to fix as soon as possible.

A SOA serial consistency test is also included in online services like the DNSreport in DNSstuff Professional Toolset. The DNSreport there goes far beyond my little script and might easily be worth its price per year. But my goal here is automation and repeatedly checking name resolution for 20+ domains via a web interface is not my idea of a good day at the office. One idea here would be to use Powershell to automatically fill the DNSreport web form, send the request and parse the HTML results. But relying on the availability and unchanging layout of a website is one dependency too much for my own taste. And since we’re talking about checking DNS here, you’ll might even end up in a situation where you have to run the tests because external web pages like the one for DNSstuff are not accessible due to current DNS configuration problems.

So what is involved in checking SOA serials for a zone? First of all we have to look up the name server (NS) entries for this zone. Then we have to ask all of these servers to tell us their SOA serial value for this zone. Finally, we can compare the results to see if there are any differences between them.

Implementation

The next questions for the implementation are: using Powershell, how do we get the name server information for a zone and how do we get these SOA serial values from different name servers listed for the zone? On the command line all this information can be obtained by employing nslookup to the task. Entering “set type=ns” at the nslookup command prompt and then querying a zone name gives you a list of the name servers registered for a zone. After that “server ” followed by one of the names tells nslookup to query this name server directly. Issuing “set type=soa” and then querying a zone name gives you the full SOA record. This record will tell you which name server is the primary one, an administrative contact and the serial value for the zone. The serial value is usually (outside the MS AD-world) constructed from a date in ISO format plus two more digits depicting the ‘version of the day’.

If you try these commands with nslookup you notice that the ‘answer format’ differs very much depending on the question being asked. We’re always getting a bunch of text lines which ‘somewhere’ contain the information we’re interested in. When using nslookup from Powershell we would have to extract this information either by using purely positional extraction (‘result line X, character position A to B’) or to use regex pattern matching (‘get the lines containing the pattern “nameserver = ” and extract everything after the equal sign’). While this is definitely a possible way to go, personally I don’t feel too well whenever I have to rely on the assumption that the result format of external applications like nslookup doesn’t change. For that reason I was looking into alternative implementations. Since DNS resolution is a very important service in every computer system, my first guess was that there must be something in the .NET Framework for that purpose. Unfortunately, System.DNS gives you only basic DNS resolution capabilities like forward and reverse lookup of host names and addresses. It won’t even allow you to specify the DNS server to use, so you’re stuck with the one which is configured for your active NIC. Fortunately a quick Google search revealed that somebody already had a go at this shortcomings of the .NET DNS class. This CodeProject page by Alphons van der Heijden is about a GUI-based .NET utility offering the functionality of the dig implementation found e.g. in the ISC BIND package. For our script we won’t use the GUI part included in this utility. But we can still use it from Powershell since .NET assemblies allow you to directly reuse public classes contained in them. And that holds true even if the assembly is an EXE file and not a DLL – which is the traditional assembly format for reuse of compiled code.

Another way to reuse the code by Alphons van der Heijden with Powerscript can be found here: Joel Bennett wrapped it up to be used straight forward as a Powershell cmdlet.

After all these explanations, here finally is the updated Powershell script:

# Test-IPResolution.ps1
# (c) Marcus Schommler, 2011
# Testing DNS resolution for multiple DNS server and multiple host names.
# The configuration and test data is read from an XML file.
# Version 2:
# -	added SOA serial value lookup option over all nameservers listed for a domain/zone
# - using a .NET based assembly implementing DNS resolving capabilities

# ==========================================
# Open issues & possible enhancements:
# - It might be of interest whether the answer of a DNS server is non authoritative.
# - Also it might be helpful to specify for test cases whether an authoritative answer is expected.
# - XML could be used for output as well...

# assertEquals()
# inspired by: http://www.leeholmes.com/blog/2005/09/05/unit-testing-in-powershell-%E2%80%93-a-link-parser/
function assertEquals (
	$expected = "Please specify the expected object",
	$actual = "Please specify the actual object"
    )
{
	if(-not ($expected -eq $actual)) {
		$res = "FAILED. Expected: $expected.  Actual: $actual."
	} else{
		$res = "OK. $actual";
	}
	$res
}

# forward_lookup(): Do DNS forward lookups by using nslookup
# inspired by: http://powershellmasters.blogspot.com/2009/04/nslookup-and-powershell.html
Function forward_lookup ($hostname, $dns_server) {
	# Build command line including stderr redirection to null device:
	$cmd = "nslookup " + $hostname + " " + $dns_server + " 2>null"
	$Error.Clear()
	$global:nonauthAnswer = $false
	$global:controlladns = $false
	$result = Invoke-Expression ($cmd)
	trap {
		$global:controlladns = $true
		$solved_ip = "0.0.0.0"
		continue
	}
	if ($Error.Count -gt 0) {
		# nslookup does output to stderr if a name resolution result is not authoritative.
		# Simplified assumption here: This is the only reason for generating error output.
		# echo "answer not authoritative"
		$global:nonauthAnswer = $true
	}

	# Line 4 of the nslookup-Output contains the resolved IP address
	# -> check and extract by (simplified) pattern matching:
	if ($result.SyncRoot[4] -match "([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)") {
		$solved_ip = $matches[1]
	}
	$solved_ip
}

# DNS query type values
# nameserver queries:
[Heijden.DNS.QType] $qtype_NS = 1
# SOA queries:
[Heijden.DNS.QType] $qtype_SOA = 6

# helper function for checking SOA serial values across nameservers:
function compare_soa_serials([REF]$last_serial, $this_serial, $zone) {
	if ($last_serial -eq $null) {
		$last_serial = $this_serial
	} else {
		if ($last_serial -ne $this_serial) {
			# the serials don't match -> note a failure
			$fails += $zone + ": SOA serial mismatch between name servers"
		}
	}
}

# function for getting the SOA serial values from all nameservers for a domain (zone)
function get_soa_serials($start_dns, $zone) {
	# get a new resolver, the parameter to the constructor specifies the DNS server to start with:
	$my_resolv = New-Object Heijden.DNS.Resolver($start_dns)
	$curr_serial = $null
	$nservs = [Heijden.DNS.Response] $my_resolv.Query($zone, $qtype_NS)
	foreach ($rr in $nservs.RecordsRR) {
		if ($rr.type -eq "NS") {
			# found a name server record
			# set the DNS server to use and query the SOA record:
			$nsdname = $rr.record.nsdname
			$my_resolv.DnsServer = $nsdname
			$res = [Heijden.DNS.Response] $my_resolv.Query($zone, $qtype_SOA)
			if ($res.header.rcode -eq "REFUSED") {
				Write-Host $zone $nsdname": query refused"
			} else {
				# there should always be only one SOA result record...
				foreach ($soa in $res.RecordsSOA) {
					Write-Host $zone $nsdname ": "  $soa.serial
					compare_soa_serials $curr_serial $soa_serial $zone
				}
			}
		} elseif ($rr.type -eq "SOA") {
			# we directly got a SOA record back
			Write-Host $zone $rr.record.mname ": " $rr.record.serial
			compare_soa_serials $curr_serial $rr.record.serial $zone
		}
	}
}

## main code section ##

# get the path of the currently running script (we need this for LoadFrom()):
$script_path = Split-Path -parent $MyInvocation.MyCommand.Definition

# We're using the DNS .NET resolver utility written by Alphons van der Heijden
# found on this CodeProject page: http://www.codeproject.com/KB/IP/DNS_NET_Resolver.aspx
# To make this script work, you just have to download http://www.codeproject.com/KB/IP/DNS_NET_Resolver/Article_Demo.zip
# and put the dnsdig.exe from the archive into the directory of this script.

# Calling Powershell V2.0 Add-Type to load an assembly won't work in our case
# since it's lacking support for EXE assemblies. So we use reflection
# to load the EXE assembly containing the resolver class to be used:
$null = [System.Reflection.Assembly]::LoadFrom("$script_path\dnsdig.exe")

# read all settings and test cases from an XML file:
$test_settings = [xml]( Get-Content .\dns-tests-2.xml )

# get start node for the dns server groups to use:
$server_groups = $test_settings.SelectNodes("ruth/dns_server_group")
# get start node for the test cases to check:
$test_sets = $test_settings.SelectNodes("ruth/dns_test_set")

# init array for storing information about failed tests:
$fails = @()

# iterate over test sets found:
foreach ($set in $test_sets) {
	# lookup up which group of DNS servers should be used for this test set:
	$sg_name = $set.getAttribute("dns_server_group")

	$check_soa_serials = $set.getAttribute("check_soa_serials")
	$check_soa_serials = ($check_soa_serials -eq "1")

	# get the individual dns server nodes for this dns server group:
	$sg = $test_settings.SelectNodes("ruth/dns_server_group[@id='$sg_name']/dns_server")

	Write-Host "Test-Set: " $set.GetAttribute("id")

	# get the actual tests to perform
	$tests = $set.SelectNodes("dns_test")
	# iterate over the servers in the server group:
	$first_dns_server = $null
	foreach ($server in $sg) {
		# iterate over the test cases:
		if ($first_dns_server -eq $null) {
			#save name of first dns server from group for later use
			$first_dns_server = $server.getAttribute("ip")
		}
		foreach ($test in $tests) {
			# extract the needed information for test execution from various xml nodes:
			$hostname = $test.getAttribute("host")
			$expect_ip = $test.getAttribute("ip")
			$dns = $server.getAttribute("ip")
			# execute the test:
			$ip = forward_lookup $hostname $dns

			# check the result of the dns resolution against the expected IP address:
			$res = assertEquals $expect_ip $ip
			# generate output:
			$s1 = "DNS: $dns, Host: $hostname : "
			if ($res.contains("FAIL")) {
				$fails += $s1 + $res
			}
			Write-Host $($s1 + $res)
		}
	}
	if ($check_soa_serials) {
		# if SOA serial value testing is to be done, we prepare a list of domains (zones)
		# by iterating once again over the test set.
		$domains = @{}
		foreach ($test in $tests) {
			$hostname = $test.getAttribute("host")
			# extract the domain part from the fully qualified host name:
			$dom = $hostname.substring($hostname.IndexOf(".") + 1)
			$domains[$dom] = 1
		}
		# now iterate over the distinct domains (zones) found:
		foreach ($zone in $domains.keys) {
			# do the SOA serial value check starting with the first dns server from the test group:
			get_soa_serials $first_dns_server $zone
		}
	}
	Write-Host "===== End Test-Set: " $set.GetAttribute("id")
}

# Output of a Summary: To have only the failed tests all in one coherent block of text,
# we repeat the output just for them:
Write-Host "============"
Write-Host "All Failures"
foreach ($f in $fails) {
	Write-Host $f
}

About these ads
Categories: powershell
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: