PowerShell: Managing DNS
In this article we will cover the way DNS works for name resolution outside if your organization. We will cover the general principles, how the NSLookup tool can be used to view that data, and in the end, we will write a custom PowerShell script to automate the use of NSLookup to validate where our External Domains are being directed to for name resolution.
The Basics
In our homes and offices, the predominate method used by computers and other devices to communicate to one another is the IP protocol. The IP version 4 protocol is still the majority holder today, but IP version 6 is fast taking its role as the new king. In both case, systems are assigned a series of numbers and/or letters that uniquely represent that system on a network.
An example of an IP version 4 address is 192.168.1.10.
An example of an IP version 6 address is FE80:0000:0000:0000:0202:B3FF:FE1E:8329.
In order to reach a system on the internet, your computer must know that system's IP address. For example, to access LinkedIn's website, your computer must know the IP address of Linkedin's web server which at the time of the writing is 199.101.163.129. If you were to put that IP address into a browser such as Internet Explorer or Chrome, LinkedIn's webpage would be displayed. This works great for computers; however as humans, we find it difficult to retain in our memory a catalog of where each IP address goes to. This is why we use DNS (Domain Name Service). DNS is a service that allows us to give a name to an IP address that is meaningful to us humans. When we need to go to Linkedin's website, we don't type 199.101.163.129, we type www.linkedin.com. When we use a name to reference a system we would like to communicate with, our computers have no idea who that name represents. Our computer reaches out to a DNS server and asks it to cross-reference the name we provided to the IP address it understands. In this case, our computer would ask the DNS server what the IP is for www.linkedin.com, and the DNS server would return 199.101.163.129. The computer would in turn use that IP address to reach out to Linkedin's webserver over the internet and retrieve Linkedin's website to your browser.
Let's use NSLookup to see this in action. NSLookup is an administration command-line tool available for many computer operating systems for querying DNS.
C:\nslookup www.linkedin.com
Server: google-public-dns-a.google.com
Address: 8.8.8.8
Non-authoritative answer:
Name: www.linkedin.com
Addresses: 2620:109:c00d:100::c765:a381
199.101.163.129
In this example, the NSLookup is querying Google's Public DNS Servers for the IP addresses associated with the name www.linkedin.com. One of the things to notice from the above is the "Non-authoritative answer:" This is because Google's DNS servers know the IP address www.linkedin.com resolves to because it has been queried for it before. When it got the answer, Google's DNS servers put that answer in its cache. The answer will stay in the cache on Google's DNS server until the TTL (Time to Live) for that answer expires. While the answer is in the cache, Google's DNS servers are able to provide a "Non-authoritative" answer to the query when asked.
So where does the authoritative answer come from? For every domain, there is a DNS server (or set of servers) that are responsible for that domain. At the time of this writing, one of those DNS Servers for linkedin.com is "ns1.linkedin.com". If we run our NSLookup command against Linkedin's DNS Server, you will see that we no longer get the "Non-authoritative answer" we got before.
C:\nslookup www.linkedin.com ns1.linkedin.com
Server: ns1.linkedin.com
Address: 156.154.64.23
Name: www.linkedin.com
Addresses: 2620:109:c00d:100::c765:a381
199.101.163.129
Alright, so now we know where the authoritative answer comes from, but now how does Google know who to ask for the authoritative answer? This is where Root Hints make an appearance. The IANA (Internet Assigned Numbers Authority) is responsible for the global coordination of the DNS Root, IP Addressing, and other Internet protocol resources. More can be found about IANA at www.iana.org. What is relevant here is that they maintain the Root Hint Servers. Root Hint servers are the authoritative name servers that serve the DNS root zone. They are a network of hundreds of servers in many countries around the world, and are configured in the DNS root zone as 13 named authorities. (https://www.iana.org/domains/root/servers) When DNS servers are either not the authoritative server for a domain, or do not have an answer in its cache, they work through their configuration asking other DNS servers where forwards, delegations, or stub zones are in place. If they cannot get an answer from any of the other sources, they end up asking the globally standardized Root Hint servers. The Root Hint Servers reply with the DNS servers that are responsible for hosting the next DNS level down, in this case the top-level domains like .com. Those DNS servers then respond with the DNS servers responsible for hosting the next level down, etc., etc. until the authoritative DNS Server is found for the query and an answer is found. Let's see this in action.
We use NSLookup to query one of the root hint servers.
C:\nslookup www.linkedin.com a.root-servers.net
in-addr.arpa nameserver = a.in-addr-servers.arpa
in-addr.arpa nameserver = b.in-addr-servers.arpa
in-addr.arpa nameserver = c.in-addr-servers.arpa
in-addr.arpa nameserver = d.in-addr-servers.arpa
in-addr.arpa nameserver = e.in-addr-servers.arpa
in-addr.arpa nameserver = f.in-addr-servers.arpa
a.in-addr-servers.arpa internet address = 199.212.0.73
b.in-addr-servers.arpa internet address = 199.253.183.183
c.in-addr-servers.arpa internet address = 196.216.169.10
d.in-addr-servers.arpa internet address = 200.10.60.53
e.in-addr-servers.arpa internet address = 203.119.86.101
f.in-addr-servers.arpa internet address = 193.0.9.1
a.in-addr-servers.arpa AAAA IPv6 address = 2001:500:13::73
b.in-addr-servers.arpa AAAA IPv6 address = 2001:500:87::87
c.in-addr-servers.arpa AAAA IPv6 address = 2001:43f8:110::10
d.in-addr-servers.arpa AAAA IPv6 address = 2001:13c7:7010::53
e.in-addr-servers.arpa AAAA IPv6 address = 2001:dd8:6::101
f.in-addr-servers.arpa AAAA IPv6 address = 2001:67c:e0::1
Server: UnKnown
Address: 198.41.0.4
Name: www.linkedin.com
Served by:
- a.gtld-servers.net
192.5.6.30
2001:503:a83e::2:30
com
- b.gtld-servers.net
192.33.14.30
com
- c.gtld-servers.net
192.26.92.30
com
- d.gtld-servers.net
192.31.80.30
com
- e.gtld-servers.net
192.12.94.30
com
- f.gtld-servers.net
192.35.51.30
com
- g.gtld-servers.net
192.42.93.30
com
- h.gtld-servers.net
192.54.112.30
com
- i.gtld-servers.net
192.43.172.30
com
- j.gtld-servers.net
192.48.79.30
Com
NSLookup returns a lot of information, but the piece we are interest in is the Served by: section. In this section, a list of DNS servers were returned that are authoritative for the ".com" top-level domain. Now we need to run our query against the .com DNS servers.
C:\nslookup www.linkedin.com a.gtld-servers.net
(root) nameserver = g.root-servers.net
(root) nameserver = h.root-servers.net
(root) nameserver = i.root-servers.net
(root) nameserver = j.root-servers.net
(root) nameserver = k.root-servers.net
(root) nameserver = l.root-servers.net
(root) nameserver = m.root-servers.net
(root) nameserver = a.root-servers.net
(root) nameserver = b.root-servers.net
(root) nameserver = c.root-servers.net
(root) nameserver = d.root-servers.net
(root) nameserver = e.root-servers.net
(root) nameserver = f.root-servers.net
Server: UnKnown
Address: 192.5.6.30
Name: www.linkedin.com
Served by:
- ns1.p43.dynect.net
208.78.70.43
linkedin.com
- ns2.p43.dynect.net
204.13.250.43
linkedin.com
- ns3.p43.dynect.net
208.78.71.43
linkedin.com
- ns4.p43.dynect.net
204.13.251.43
linkedin.com
- ns1.linkedin.com
156.154.64.23
linkedin.com
- ns2.linkedin.com
156.154.65.23
linkedin.com
- ns3.linkedin.com
156.154.66.23
linkedin.com
- ns4.linkedin.com
156.154.67.23
linkedin.com
In the Served by: section, NSLookup now returns the DNS servers responsible for linkedin.com. So we now query one of DNS servers for linkedin.com and get the authoritative answer to our query for www.linkedin.com.
C:\nslookup www.linkedin.com ns1.linkedin.com
Server: ns1.linkedin.com
Address: 156.154.64.23
Name: www.linkedin.com
Addresses: 2620:109:c00d:100::c765:a381
199.101.163.129
Ok, so now we know how we get an authoritative answer for a record, and we know that the Root Hints are maintained as a Global Standard by the IANA, but how do the Root Hints know which DNS servers are responsible for the "top-level" domains like com, org, and net? IANA is responsible assigning the operators of top-level domains, such as .uk and .com, and maintaining their technical and administrative details. (https://www.iana.org/domains/root) Those operators access IANA's system to update and maintain the list of Authoritative DNS servers for those top level domains. Those operators can be found in IANA's Root Zone Database (https://www.iana.org/domains/root/db). The operators of the top level domains then contract with Accredited Registrars. We then purchase domain names and manage the list of DNS server for those domains via the Registrar. The Registrar is then responsible for updating the DNS servers for the "top-level" domains DNS servers with those changes. This completes the circle on how name resolution out on the internet takes place.
If you have access to your registrar, you can easily log into their management system and see what DNS servers are configured for your domains. I, however, work for a very large enterprise where management of our DNS environment is separate from management of our purchased internet domains. We recently had to go through the process of deploying and migrating to a new DMZ infrastructure, which included migrating our external facing DNS servers to the new DMZ. We deployed new DNS servers in the new DMZ, but I had no access to the Registrar to see the current configuration or update our domains to point to the new DNS servers. If you are in this same situation, you can now see how you can use NSLookup to see where the Root Hints are directing DNS queries for your domains. For a hand full of domains, working through this process will work effectively. In my situation, I had over 1000 domains that I needed to validate.
A Couple of Whys?
Before moving on, I feel it necessary to cover a couple of quick topics from some questions that have been asked. 1) Why we would use this process to find our authoritative servers as opposed to just using NSLookup to query for NS records?, and 2) What about the Resolve-DNSName cmdlet available with PowerShell 4.0? We will answer these questions and will pick back up on the automation of NSLookup in the next post.
Why we would use this process to find our authoritative servers as opposed to just using NSLookup to query for NS records?
Using NSLookup, it is possible to specify a DNS query to return the NS "Name Server" records for a domain.
Using the manual process of querying from the root hint gives us:
https://media.licdn.com/mpr/mpr/shrinknp_400_400/p/4/005/0a9/207/0d2ecb3.png
The root servers give us references to the .com DNS Servers
https://media.licdn.com/mpr/mpr/shrinknp_800_800/p/8/005/0a9/207/172dc2c.png
And the .com DNS Servers give us the Name Servers that are configured for the linkedin.com domain
https://media.licdn.com/mpr/mpr/shrinknp_800_800/p/6/005/0a9/207/3923696.png
Looking at the results, the NSLookup query for the NS records returns the same list of name servers for linkedin.com as the manual process of working through the root hints. This can be very deceptive, however, if you do not completely understand what is going on in the background. The NS records are part of the domain zone configuration. What this means is that in order to get the NS records, the following happens:
- Workstation sends DNS Query for NS Records to its configured local DNS Servers
- Local DNS Server follows its configured cache, forwarding, and recursion rules
- If not found in any cache, the Query eventually gets to the root hint servers
- The root hint servers return a reference to the .com DNS servers
- A query for the NS records is then sent to the .com DNS servers
- The .com DNS servers return a reference to the linkedin.com DNS servers
- A query for the NS records is then sent to the linkedin.com DNS servers
- The linkedin.com DNS servers return the authoritative query results containing the configured NS records for the domain zone
As you see from the outlined steps above, querying the NS record goes through the same process we manually work through using NSLookup. This difference is that two additional steps (7 & 8) are taken to obtain the NS records. If the domain zone and registrar are configured correctly, the list returned by step 6 should match the list returned by step 8. Since these lists are maintained separately, there is a possibility for a misconfiguration to occur where the Registrar is pointing to different Name Servers than the NS records as demonstrated below.
An NSLookup query for the NS records of quickstep.ee returns (ns03.mohakwind.com, ns04.mohawkind.com, and ns05.mohawkind.com)
Using the Manual process of querying the root hints gives us:
https://media.licdn.com/mpr/mpr/shrinknp_400_400/p/2/005/0a9/208/203933c.png
The root servers give us references to the .ee DNS Servers
https://media.licdn.com/mpr/mpr/shrinknp_800_800/p/3/005/0a9/208/092b3e5.png
And the .ee DNS Servers give us the Name Servers that are configured for the quickstep.ee domain at the registrar (ns01.mohawkind.com, ns02.mohawkind.com, and ns03.mohawkind.com)
https://media.licdn.com/mpr/mpr/shrinknp_400_400/p/5/005/0a9/208/29f4413.png
As you can see, the DNS servers returned by the .ee DNS servers does not match the DNS servers returned by the NS record query. Since the Registrar does not update the domain glue records based on the NS records in the zone configuration, we cannot rely on the NS records to accurately indicate what DNS servers a domain will be directed to from the root-hints. We must query through the root hints to guarantee we are obtaining good data.
What about the Resolve-DNSName commandlet available with Powershell 4.0?
Resolve-DNSName is a cmdlet that was introduced to PowerShell in version 4.0 on Windows OS versions 8.1, 2012 R2, and newer. Although PowerShell 4.0 can be installed on Windows 7 workstations, the Resolve-DNSName cmdlet is not available for Windows 7 and older operating systems. At the time of this writing, my company has not opted to move forward with the adoption of the Windows 8 Operating system. I suspect this will be the same for many other companies that read this post. NSLookup has been part of the Windows Operating system since the Windows NT 4.0 days and therefore is available to anyone using any relatively recent version the OS. At this stage in the game, it makes since to stay with what is available. I did work with the Resolve-DNSName cmdlet, however, to scope it for future use. In its current iteration, it appears to get a good cmdlet for basic DNS resolution. It allows a lot of the similar query customizations that NSLookup allows such as querying against specific DNS servers or for specific record types. Unfortunately the commandlet does not appear to support "served by" results. In situations where NSLookup would display results where the DNS server returns a list of alternate DNS server where the domain zone is hosted, Resolve-DNSName errors out. This makes the Resolve-DNSName cmdlet unusable for our need.
NSLookup to a root hint server
https://media.licdn.com/mpr/mpr/shrinknp_800_800/p/5/005/0a9/209/0e70a21.png
Resolve-DNSName to a root hint server
https://media.licdn.com/mpr/mpr/shrinknp_800_800/p/4/005/0a9/209/11d2c98.png
With that settled, we will start developing a PowerShell script to automate the NSLookup command for the purpose of making the lengthy NSLookup process a simple PowerShell cmdlet.
String Methods
We will begin by looking at a few of methods PowerShell makes available for strings as we will be using these methods in our script. If you are very familiar with working with String in PowerShell, you can skip down to "Capturing our NSLookup Output".
Substring
Substring is a PowerShell method that is exposed by a String object. It allows you to get part of a string by specifying various parameters.
Usage: string Substring(int startIndex)
string Substring(int startIndex, int length)
In the example below we show a string by itself, then we use the Substring method with one parameter to only show the part of the string remaining after skipping 6 characters. Lastly we show the usage of the Substring method with two parameters. The first parameter acts the same as before, so it skips the first 6 characters, however, the second parameter tells it to stop after collecting the after next 7 characters.
Trim
Trim is a PowerShell method that is exposed by a String object. It allows you to remove white space from the beginning and ending of a string, or remove all leading and trailing occurrences of a set of characters specified in an array from the String.
Usage: string Trim(Params char[] trimChars)
string Trim()
In the example below we show a 3 different strings concatenated together. The first string ends with a space, the second string begins and ends with 4 spaces, and the third string begins with a space. When shown as is, you can see the words are very separated because of all of the spaces. We then use Trim on the second string. Trim removes all of the white space, in this case space characters (could also be tab, linefeed, carriage-return, form feed, vertical-tab, and newline). Since trim removes the space characters from the beginning and ending of the second string, you can see the resulting output only contains the spaces that were at the end of the first string and the beginning of the third string.
Contains
Contains is a PowerShell method that is exposed by a String object. It allows you to determine if a string has a certain character or series of characters in it.
Usage: bool Contains(string value)
In the example below we show the string by itself, then we use Contains to determine if the characters "Char" are found in the string. That results in a True response. Lastly we use Contains to determine if the characters "Ash" are found in the string. That results in a False response. It is worth noting that Contains is case sensitive. If we tried to find "char" in the string, it would result in a False response.
StartsWith
StartsWith is a PowerShell method that is exposed by a String object. It allows you to determine if the string begins with a certain character or series of characters.
Usage: bool StartsWith(string value)
bool StartsWith(string value, System.StringComparison comparisonType)
bool StartsWith(string value, bool ignoreCase, cultureinfo culture)
In the example below we show the string by itself, then we use StartsWith to determine if the characters "Hello" are found at the beginning of the string. That results in a True response. Lastly we use StartsWith to determine if the character "G" is found at the beginning of the string. That results in a False response. It is worth noting that StartsWith is case sensitive unless it is specifically instructed to ignore case. It is also worth noting that StartsWith returns True when checking an empty string. For example: "Hello Charlie Brown".StartsWith("") returns True.
EndsWith
EndsWith is a PowerShell method that is exposed by a String object. It allows you to determine if the string ends with a certain character or series of characters.
Usage: bool EndsWith(string value)
bool EndsWith(string value, System.StringComparison comparisonType)
bool EndsWith(string value, bool ignoreCase, cultureinfo culture)
In the example below we show the string by itself, then we use EndsWith to determine if the characters "own" are found at the end of the string. That results in a True response. Lastly we use EndsWith to determine if the character "G" is found at the end of the string. That results in a False response. It is worth noting that EndsWith is case sensitive unless it is specifically instructed to ignore case. It is also worth noting that EndsWith returns True when checking an empty string. For example: "Hello Charlie Brown".EndsWith("") returns True.
Capturing our NSLookup output
To begin our script we will work on capturing the output of NSLookup and parsing through it to pull the values we are looking for. Since the NSLookup command is used several times in our manual process, it will need to be called several times throughout the runtime of our finalized script. For this reason we will start our script by defining a "ParseNSLookup()" function this function will accept the output of the NSLookup command as input in order to parse it.
In the code above, we have setup our basic building blocks for working on our script. Lines 2 - 5 contain the ParseNSLookup function that at this point: accepts 2 parameters of input. $nsldata will contain the data sent to the function that has the nslookup command output, and $domain will contain the domain that was original queried using nslookup. Line 3 sets the $result variable to the content of the $nsldata variable, and line 4 returns the $result variable. Lines 6 and 7 are simply there to give us data to work with. Line 6 runs the NSLookup command and stores the output of that command in the $output variable. Line 7 calls the ParseNSLookup function and passes the $output variable to the function at the $nsldata parameter, and the domain we are querying for, at the $domain parameter.
Now that we have the building blocks, let's begin working with our data. Looking at the screenshot below, we identify the section of the output that we are interested in. In this case the section under "Served by:" holds the data we want. Also looking at the data, where our relevant data begins, we have the first instance of a line beginning with "-". We can use this in our script as a starting point for analyzing the data. Additionally, we see that there are multiple "records" in the same output. Each record begins with "-". We can again use this dash to indicate a separation of records.
In order to begin analyzing the data, we need to add the building blocks for looking through our NSLookup data line by line.
In the code above, just like before, our function accepts the input parameter $nsldata. On line 3, however, we are now taking that input and piping it into the "ForEach-Object" cmdlet. In this scenario, each individual line of text that was submitted as the input is broken down into an individual string object. With this setup we can analyze the input data line by line. The first thing we need to do is find that first "-". It will be the indicator that we have reached our relevant dataset. Once the dataset is found we need to begin doing additional analysis on all of the remaining lines of input.
In the code above, on line 3, we've declared a new variable called $FoundDataSet and set its initial value to $False. Then on line 5 we use an If statement to determine if the $FoundDataSet variable is anything other than $False. If is anything other than $False, then we will do some action. Otherwise, if $FoundDataSet is $False, Check to see if the current line starts with a "-".
Before we go on, let's talk about "$_". "$_" is a special variable. When used within the ForEach-Object cmdlet, "$_" represents the current object. In our scenario, the first time through "$_" would have a value of "in-addr.arpa nameserver = f.in-addr-servers.arpa" since it is the first line of our input from the $nsldata variable. After processing all of the code within the curly brackets for the ForEach-Object cmdlet for the first line of input, the "$_" variable is updated to be "in-addr.arpa nameserver = e.in-addr-servers.arpa" since that is the second line of our input. This will continue until all of the input has been processed line by line.
So, if the current line begins with "-", then we set the variable $FoundDataSet to $True.
Now let's look at the data set record
Each record contains the Fully Qualified Domain Name of the DNS Server on the same line as the "-". Subsequently you may find an IPv4 address and/or and IPv6 address followed by the domain that this DNS server is configured to serve. This could be the domain we are querying for (ex. Linkedin.com), or it could be one the parent domains of the domain we are querying for (ex. com).
What this means is that when we find the dash, we must also capture the FQDN of the DNS server from the string at that time.
In the code above we've added lines 10 and 11 to be processed after we find our first "-". On line 10 we create a new object called $dnsserver and specify fqdn, domain, ipv4, and ipv6 as its properties. On line 11, we use the substring method, discussed earlier, to skip the first two characters (the dash and the space) of the current line and keep everything else. The result is stored in the fqdn property of the $dnsserver object. Showing the contents of the $dnsserver object would show fqdn populated with a value and the remaining properties empty.
Since we have now identified the starting point of our data we must begin looking more deeply at all of the remaining lines of input. By setting the $FoundDataSet variable to $True, the If statement on line 5 will now resolve $True and the remaining lines of input will be processed by the code that replaces the #<do something> marker rather than the code on lines 7 - 12.
Looking back at our data, we have to determine how to identify the different data types.
FQDN was easy because it always began with a "-".
IPv6 is also easy because it always contains a ":" and it is the only thing that contains a ":"
IPv4 and Domain, however, are going to be a little more complicated. You could seemingly say things with a "." in them are IPv4, but domain names can also have "." in them. In order to identify them properly we will have to check the values in a specific order.
First we will check to see if the domain we are querying for ends with the current line. For example, does linkedin.com end with com? If that is true then the line must be part of the domain. If it is not true, for example linkedin.com does not end with 192.5.6.30, then we check to see if it contains a period. If the line is not part of the domain we are querying and it contains a ".", then it must be an IPv4 address.
In the code above, we have created our test scenarios to determine the data type that is in the current line of input. First we check to see if it begins with a dash (Line 6). If it does, we know it is a new record and later we will fill in what to do when new records are found. If not, trim all the white space off of the beginning and ending and then check if the remaining value exists at the end of the domain we are querying for (Line 8). If it does, then we have found a domain value and we store it in the domain property of the $dnsserver object. If it is found to not be part of our query domain, then we check to see if it contains a period (Line 10). If a period is found then it must be an IPv4 address since we have already confirmed it is not part of the domain. Since it is an IPv4 address, we trim it and store the value in the ipv4 property of our $dnsserver object. Lastly, if it is found to not have a period, the check to see if it has a colon (Line 12). If a colon is found then it must be an IPv6 address. Since it is an IPv6 address, we trim it and store the value in the ipv6 property of our $dnsserver object.
The result after processing the first record is a $dnsserver object with all of its found properties populated.
Now we need to create the process of handling a new record. When a new record is found we will need to store the current record's $dnsserver object and then generate a new object to collect the properties for the new record.
In the code above we have added the process for handling a new record. On line 8, we add the current $dnsserver object to the $result variable that will be returned by the function. In order to do that, however, we had to make a small adjustment. On line 4 we added: https://media.licdn.com/mpr/mpr/shrinknp_100_100/AAEAAQAAAAAAAAFwAAAAJDZlM2EyZTI2LTlhNzgtNDU0NS05MjU5LWZkZTUyNmEzMTBkZg.png This initiates the $results variable as an array, or collection, so multiple objects can be added to it. If it were not initiated as an array, an error would be generated on line 8 when the += was processed since this functionality is not valid with string variables.
With this last piece done, we have completed our NSLookup parsing function. The result of running this function is a returned collection of all the name servers provided by the NSLookup command including the FQDN, IPv4 Address, IPv6 Address, and Domain they are hosting.
Result:
From:
Now that we have developed a function to parse the output of the NSLookup command for the DNS Servers that host the requested domain, we will develop a new function to facilitate our initial query to the root hint servers by sending the NSLookup output to the ParseNSLookup function, and then handling the necessary recursive queries until our requested domain is found.
Developing the QueryDNS Function
We will start our new function by defining its basic building blocks.
In the code above, we have defined our function to accept two input parameters. $domain is the domain name that we wish to find the registrar's configuration for. $dnslist is a list of DNS Server Objects that we want to query to find our domain name. We have setup two scenarios. We check to see if a $dnslist is provided. If it has been provided, then we will insert code to perform the DNS Queries against the provide list. Otherwise, we will insert code to perform the DNS Queries against the root hint servers. (https://www.iana.org/domains/root/servers).
In the code above, we have moved our sample working data section from the previous post into our new QueryDNS function. We then placed a call to our new QueryDNS function in the main body of our script, where the sample data code used to be, passing in the domain we are ultimately querying for. Next we need to capture the result of our ParseNSLookup function in order to decide what to do next.
In the code above, on line 7 we have set the result of the ParseNSLookup function to be stored in a variable called $result. On line 8 we check to see if $result contains any data. If it does then we will add code to evaluate that data, and potentially return the result. If $result does not contain any data, then our nslookup was not successful. While it is highly unlikely that the root hint servers will become unavailable, it is possible. We will add functionality to address this possibility. Based on the IANA, the root hint servers are a.root-servers.net through m.root-servers.net. (https://www.iana.org/domains/root/servers) In order to handle any one of these root hints being unavailable, we will need to loop through the list until one of them provides a valid result. Unfortunately PowerShell does not provide way to do a range of alphabetic characters. It does however provide a way to do a range of numerical characters as well as a way to get the ASCII character based on its ASCII numerical value.
In the code above, we loop through a number range from 97 to 109 and show the numerical value on ".root-servers.net", then we show the result of using the [char] strict type cast to display the ASCII character value associated to the ASCII numerical index. As you see, this allows us to create a loop where we can provide the possibility to process against all published root hint servers. For a nice reference on using this method for getting an alphabetical range, see (http://etcforge.com/2011/08/alphabet-range-sequences-in-powershell-and-a-usage-example)
In the code above, we have inserted our loop and strict type. Since we already have our results check in place, if results are found, those results are returned and the loop is exited automatically. If no results are found, then the next root hint server in the loop is queried until either a result if found or all root hint servers are tired and no results are returned.
Next we need to evaluate our result data from the ParseNSLookup data to determine if we have the data we are looking for or if we need to issue another query against the returned DNS Servers. Let's look back at an example of the result data we get from our ParseNSLookup function. Below is the result of querying the "a.root-servers.net" for "linkedin.com". We see that the list contains 10 potential DNS Servers to query for the ".com" top-level domain.
What is important to recognize is that we queried for "linkedin.com", but the result was for ".com". We need to be able to recognize this and perform recursive queried until our queried domain is found.
In the code above, we have added an if statement at line 10 to check the "domain" value of the first DNS Server Object. If that value equals the domain we originally queried for, then we have found our desired results and return them. If no match is found, when we run our QueryDNS function again, only this time, we pass in the DNS Server List that was returned from our ParseNSLookup function. Since we are passing in a DNS Server List this time, the if statement on line 3 will resolve true and we can now insert code to handle the recursive DNS queries as opposed to defaulting to the root hint servers.
In the code above, since we have a list of DNS Servers provided, we do not have to generate a list like we did for the root hints. On lines 4 through 6, we create a simple foreach loop to increment through the provide DNS Servers List, $dnslist, and run our same NSLookup command against the new DNS Server List using the fqdn value for the current DNS Server record. The rest of the process is the same as if we queried the root hint servers. We will need to pass the result of our NSLookup of to the parse function, validate we have returned data from the parse, and either return the results found or perform a recursive query.
In the code above, we have simply copied and pasted the code from lines 18 through 25 to lines 6 through 13. With that last piece in place, we have completed our QueryDNS function. As a result, running the script now returns our desired end results. We now have the DNS Servers that are configured at the Registrar for the linkedin.com domain.
Here is the full code at this stage of the project
We now have a fully functional script to obtain the specific data we are looking for; but in its current state, the script requires editing each time we want to look up a new domain. We will add code to make out script look and feel like a PowerShell Cmdlet. We will add the ability to pass in a domain, a list of domains, or an input file from the PowerShell prompt and get our registrar configured name servers as a result. It's worth noting that we are only making our script behave like a PowerShell Cmdlet when parameters are passed in from the prompt. We are not adding support for pipeline input, however, that is something for you to expand on in the future for your environment!
Accepting Input from the PowerShell Prompt
The first thing we need to do is determine what type of input we want to accept for our script. The three input types I have identified are:
- Single Domain Name passed in at the prompt
- List of Domain Names passed in at the prompt
- List of Domain Names passed in from a text file
The first two types can be accommodated by using an Array. If a single domain is passed in, we will just have an array with only one value, but that array can grow to accept as many values as we wish to pass in. We will begin by adding functionality to accept the domain and domain list inputs.
At the beginning of our script we have added the Param block. The Param block allows us to specify how to collect input from the shell prompt. On line 2 we specify that our input parameter at Position 0 (the first parameter after the script) is Mandatory, and that it belongs to the 'StringInput' Parameter Set. The position and mandatory values are pretty self-explanatory, but the Parameter Set is something special. Place it on the back burner for right now. We will discuss its purpose a little later. Then on line 3, we define that the input will be an Array that is assigned to the $domain variable by initializing it as a "strongly-typed array". On line 62, we commented out our direct function call to QueryDNS so that we could add functionality to hand the domain input from the parameter. On line 63, we simply pipe the $domain variable into the ForEach-Object cmdlet. Then for each domain string in the $domain array, we call our QueryDNS function for the current string.
Shown above, the result is we can now execute our script from the shell and simply pass in the domain we would like to query for.
We can also pass multiple domains into our script from the shell by separating them with a comma. With five lines of code we have already taken care of the first two input types for our script.
Now we need to hand file input so that a large list of domains can be supplied to our script from a simple text file. This is where we run into a conflict. We have already setup our input Param block to accept input for domains typed at the prompt, and we don't really need to type domains at the prompt if we are going to specify an input file. This means that Position 0 for the input parameters might not be a domain, it might be a path to a file. Remember the "ParameterSetName" that we put on the back burner earlier? That will now come in to play as we need to specify a different parameter set if we want to specify a file.
To allow input of a string that represents the path to a file for use for input, we have added a new Parameter Set called "FileInput". Rather than being a collection of strings, $file accepts just a single string as input. Since there are now two possible parameter sets to use, what happens when we run our script and pass in a domain from the shell like before?
As you can see, we get an error because PowerShell does not know which parameter set to use for the passed in value. In order to identify the correct parameter set, we need to identify a parameter that is unique to one of the parameter sets. The parameter name, unless specified otherwise, will be the name of the variable that will contain the value, without the $. If we desired, we could specify the "alias" attribute to give it a different name. We have not specified an alias, so our parameter names are -domain and -file.
By specifying the -domain parameter, PowerShell now knows that the "StringInput" Parameter Set is the correct set to use for our value since -domain is unique to that Parameter Set. This means that we can also specify the -file parameter and PowerShell will know to use the "FileInput" Parameter Set, and set the $file variable to the value that is passed in from the shell. If we use -file, however, we will get no results as we have not added any code to handle the reading of the input file. Let's do that now.
Since we now have two scenarios, we have to determine which scenario we are working with. We use a simple if statement to first determine if the $domain variable is set. If it is then we run our code as before against the $domain array. However, if $domain is not set, we check to see if the $file variable is set. If we find that the $file variable is set, we use the get-content cmdlet to read the content of our input file and pipe the content into the ForEach-Object cmdlet where we then call our QueryDNS function for each line item in the input file. With this functionality now in place, we can now use the -file parameter to use an input file for our process.
To put the final touch on our script we will define a default parameter set. As we have shown before, if no parameter is specified, the script will error out because I does not know what parameter to use. It makes since to me to make the domain list parameter set the default to be used if no parameter is specified.
By specify the DefaultParameterSetName of the CmdletBinding Parameter, we have told our script to default to the StringInput parameter set. Now we can simply specify an input string with no parameters and it will be seen as a domain rather than an input file. This completes the development of our script. While the catalyst for the script's creation was a need to validate Registrar configuration of our domains during a migration effort, there are other applications application for this utility.
Applications for Get-DomainDNS
Migration Monitoring and Configuration Reporting
When changing your name server for external resolution, you can easily use this script to monitoring your Registrar's progress in making the name server changes on the root hint servers. Issuing the script against a list of domains from an input file and writing the results out to a CSV will provide good data for reporting on progress or simply cataloging configuration at a given time.
Example:
.\Get-DomainDNS.ps1 -file domains.txt | Export-CSV DomainConfiguration_3-21-2015.csv
Result:
New Domain Deployment or Existing Domain Troubleshooting
The script can be used to validate your expected Name Servers are deployed to the Root Hint Serves by your Registrar after purchasing a new domain, or while troubleshooting name resolution on an existing domain.
Example:
.\Get-DomainDNS.ps1 "linkedin.com"
Result:
Expired Domain Monitoring and Alerting
A lot of domains, when expired, will be redirected to temporary name servers. Those temporary name servers point the domains to a page that indicates the domain has expired, allowing the owner to realize the domain is expired and renew it before it is release to be purchased by the general public. You can use this script to monitor if your domains are pointing to one of the "temporarystatus" name servers, and send an email alert if one is found.
Example:
$body = .\Get-DomainDNS.ps1 -file domains.txt | Where-Object ($_.fqdn -like '*temporarystatus*'} | ConvertTo-HTML; if ($body){Send-MailMessage -To "recipient@mail.domain" -Subject "Domain Name Server Registration May be Compromised" -BodyAsHtml ($body | out-string) -smtpserver "mail.server.domain" -from "sender@mail.domain"}
Result:
Domain Compromise Monitoring and Alerting
It is my opinion that External Domain Name Server registrations are a vastly under monitored attack surface. If an attacker successfully compromises the registrations for your external name server, by updating the root hints to point to their own name servers, an attacker can easily compromise the integrity of all of your website traffic. The attacker could transparently direct visitors of your sites to a server they administer and simply proxy the web access back to your web servers. This could be completely transparent to the end user, but would provide a man in the middle opportunity for the attacker to see sensitive data, inject data of their own, or even deploy malicious software to the clients that normally trust our sites. This script can be used to periodically check the name server registrations, and if a name server is found that is not one of your defined and expected name server, an alert can be generated so investigation can be performed.
Example:
$body = .\Get-DomainDNS.ps1 -domain "linkedin.com" | Where-Object ($_.fqdn -ne "ns1.linkedin.com" -AND $_.fqdn -ne "ns2.linkedin.com" -AND $_.fqdn -ne "ns3.linkedin.com" -AND $_.fqdn -ne "ns4.linkedin.com"} | ConvertTo-HTML; if ($body){Send-MailMessage -To "recipient@mail.domain" -Subject "Domain Name Server Registration May be Compromised" -BodyAsHtml ($body | out-string) -smtpserver "mail.server.domain" -from "sender@mail.domain"}
Result:
Final Notes
It is worth noting the since our script is built entirely on the NSLookup command, the future success or failure of our script depends on Microsoft not changing how NSLookup formats it's return data, and also not removing it from future releases of the operating system. For now, let's just hope that does not happen.
My hope is that you will find this finalized script useful, if not for the script itself, then at least for a better understanding on how you can use the very powerful PowerShell environment to automate your manual tasks. There are four use cases I have identified for this particular script. If there are others I have not considered, or improvements you have made for your environments, please leave a comment and let me know. Below is a full text copy of the script so you can easily copy and paste for your use. Thanks for reading!
Final Code
# Define Default Parameter Set for Shell Input of Parameters
[CmdletBinding(DefaultParameterSetName='StringInput')]
Param(
# Define Parameter set for Shell input of a list of domains to run the
# process against
[Parameter(Mandatory=$true, Position=0, ParameterSetName = 'StringInput')]
[string[]]$domain,
# Define Parameter set for Shell input of an Input File that contains a
# list of domains to run the process against
[Parameter(Mandatory=$true, Position=0,ParameterSetName = 'FileInput')]
[string]$file
)
# Define QueryDNS function that will query the root hint servers if no DNS
# Server list is provide, otherwise it will query the DNS Server list
function QueryDNS($domain, $dnslist) {
# Check if DNS Server list is provide and either use that list or query
# root hints
if ($dnslist) {
foreach ($dns in $dnslist) {
# Run the NSLookup command and capture it's output to the $output
# variable, then send that output to be parse by the ParseNSLookup
$output = nslookup $domain $dns.fqdn
$result = ParseNSLookup $output $domain
# Check if the resultant data from the ParseNSLookup function is for
# our queried domain. If it is then return it, otherwise recursively
# run QueryDNS until our queried domain is found
if ($result) {
if ($result[0].domain -eq $domain) {
return $result
} else {
return QueryDNS $domain $result
}
}
}
} else {
# Root hint servers are a.root-servers.net through m.root-servers.net
# In order to facilitate looping through a to m for the root hint server,
# we have to create a foreach loop to progress through integers 97 through
# 109. 97 is ASCII character a. We use the strict type case to convert that
# number to the associated ASCII character and concatenate it to our string.
foreach ($asciinum in 97..109) {
# Run the NSLookup command on our root hint servers, and capture it's
# output to the $output variable, then send that output to be parse by
# the ParseNSLookup
$output = nslookup $domain "$([char]$asciinum).root-servers.net"
$result = ParseNSLookup $output $domain
# Check if the resultant data from the ParseNSLookup function is for
# our queried domain. If it is then return it, otherwise recursively
# run QueryDNS until our queried domain is found
if ($result) {
if ($result[0].domain -eq $domain) {
return $result
} else {
return QueryDNS $domain $result
}
}
}
}
}
# Define ParseNSLookup function that will accept the evaluate the data passed in
# to identify the domain the returned data is for, and the associated name servers
# for that identified domain
function ParseNSLookup($nsldata, $domain) {
# Initialize the $FoundDataSet variable to identify when we have reached the
# point in our NSLookup data that we need to start evaluating
$FoundDataSet = $False
# Initialize $result as an empty array so we can add DNS Server objects to it
$result = @()
# Pipe our input NSLookup data into a ForEach-Object to looks at the data line
# by line
$nsldata | ForEach-Object {
# Determine if we have previously reached our relevant data. If True analyze
# the data more deeply, otherwise check to see if we have reached the first
# line of our relevant data
If ($FoundDataSet) {
# If line starts with a dash, we are at the beginning of a new record. We
# will add the current record to the $result array and then create a new
# record object to collect the data for the new record
If ($_.StartsWith("-")) {
$result += $dnsserver
$dnsserver = New-Object psobject -Property @{fqdn = ""; domain = ""; ipv4 = ""; ipv6 = ""}
$dnsserver.fqdn = $_.substring(2)
# If the current line is not blank, is part of our queried domain, then
# we know this is the domain property of our record
} elseif ($_ -AND $domain.endswith($_.Trim())) {
$dnsserver.domain = $_.Trim()
# If we have gotten here, then the current line is not a domain property,
# so if it has a "." then it is the ipv4 address
} elseif ($_.contains(".")) {
$dnsserver.ipv4 = $_.Trim()
# If the current line contains a ":", then we know it is the ipv6 address
} elseif ($_.contains(":")) {
}
} Else {
# In the NSLookup input data, we identify the beginning of our relevant
# data by finding the first line that begins with a "-". When that "-"
# is found, we set our $FoundDataSet to True so we begin analyzing each
# subsequent line more deeply. We also create our first DNS Server object
# and populate the fqdn value from the current line
If ($_.StartsWith("-")) {
$FoundDataSet = $True
$dnsserver = New-Object psobject -Property @{fqdn = ""; domain = ""; ipv4 = ""; ipv6 = ""}
$dnsserver.fqdn = $_.substring(2)
}
}
}
# At this point, we have gone through all of the input data. We add the last record
# to the $result array and then return the array as the output of the function
$result += $dnsserver
return $result
}
# This is our main code block. It first check to see if we are working with domain input
# from the shell by determining if the $domain variable is set, or if we are working with
# file input by checking if the $file variable is set. These variables are set by the
# Parameters block depending on what parameters are input at the shell when the script
# is executed
if ($domain) {
# Since Domain input was identified, pipe that input and for each domain call
# our QueryDNS Function
$domain | ForEach-Object {QueryDNS $_}
} elseif ($file) {
# Since File input was identified, read the content from the file, pipe content
# and for each line (which is a domain in the list) call our QueryDNS Function
get-content $file | ForEach-Object {QueryDNS $_}
}