Thursday, September 07, 2006

Powershell DNS Utility script for querying MX, PTR and SPF records

One thing as an email administrator that I find myself doing a lot is using nslookup to track down various problems with receiving and sending email. Whether its using it to look up a MX record , or to see if someone has a reverse DNS entry or maybe look at a SPF record if I’m looking at a sender ID issue. Now while I like nslookup I do find this utility a little cumbersome and slow to use at times so I started looking into seeing if I could automate this a little with a script. Doing this from a script isn’t the easiest thing in the world Peter Bromberg and Bill Jamieson came up with a great C# class for doing DNS queries which I’ve adapted and modified to do the particular things that I wanted. Because this class uses some Win32 API functions I’ve used Jeffrey Snovers method from here to make use of these functions in Powershell.

So the code is a compilation of bits and pieces from the sources above with some of my own touches. I’ve added an extra A record lookup for each of the hostnames that are returned from the MX query. I’ve also added some routines into to lookup and display SPF records. As well as including other routines to format the IP address correctly to perform reverse lookups.

The script can basically do 4 types of queries it takes 2 command line parameters the first is the type of query (A,MX,PTR,SPF) and second is the IP or hostname an example of running it is



I’ve put a downloadable copy of the code here the code itself looks like.

param([String] $dnsqtype = $(throw "Please specify the DNS Query Type"),[String] $IpParam = $(throw "Please specify the IPaddress"))

function Compile-Csharp ([string] $code, [Array]$References) {

# Get an instance of the CSharp code provider
$cp = New-Object Microsoft.CSharp.CSharpCodeProvider

$refs = New-Object Collections.ArrayList
$refs.AddRange( @("${framework}System.dll",
"${PsHome}\System.Management.Automation.dll",
"${PsHome}\Microsoft.PowerShell.ConsoleHost.dll",
"${framework}System.Windows.Forms.dll",
"${framework}System.Data.dll",
"${framework}System.Drawing.dll",
"${framework}System.XML.dll"))
if ($References.Count -ge 1) {
$refs.AddRange($References)
}

# Build up a compiler params object...
$cpar = New-Object System.CodeDom.Compiler.CompilerParameters
$cpar.GenerateInMemory = $true
$cpar.GenerateExecutable = $false
$cpar.IncludeDebugInformation = $false
$cpar.CompilerOptions = "/target:library"
$cpar.ReferencedAssemblies.AddRange($refs)
$cr = $cp.CompileAssemblyFromSource($cpar, $code)

if ( $cr.Errors.Count) {
$codeLines = $code.Split("`n");
foreach ($ce in $cr.Errors) {
write-host "Error: $($codeLines[$($ce.Line - 1)])"
$ce | out-default
}
Throw "INVALID DATA: Errors encountered while compiling code"
}
}

$code = @'
namespace PAB.DnsUtils
{
using System;
using System.Collections;
using System.ComponentModel;
using System.Runtime.InteropServices;
public class Dns
{
public Dns()
{
}

[DllImport("Dnsapi", EntryPoint="DnsQuery_W", CharSet=CharSet.Unicode, SetLastError=true, ExactSpelling=true)]
private static extern Int32 DnsQuery([MarshalAs(UnmanagedType.VBByRefStr)]ref string sName, QueryTypes wType, QueryOptions options, UInt32 aipServers, ref IntPtr ppQueryResults, UInt32 pReserved);
[DllImport("Dnsapi", CharSet=CharSet.Auto, SetLastError=true)]
private static extern void DnsRecordListFree(IntPtr pRecordList, int FreeType);

public enum ErrorReturnCode
{
DNS_ERROR_RCODE_NO_ERROR = 0,
DNS_ERROR_RCODE_FORMAT_ERROR = 9001,
DNS_ERROR_RCODE_SERVER_FAILURE = 9002,
DNS_ERROR_RCODE_NAME_ERROR = 9003,
DNS_ERROR_RCODE_NOT_IMPLEMENTED = 9004,
DNS_ERROR_RCODE_REFUSED = 9005,
DNS_ERROR_RCODE_YXDOMAIN = 9006,
DNS_ERROR_RCODE_YXRRSET = 9007,
DNS_ERROR_RCODE_NXRRSET = 9008,
DNS_ERROR_RCODE_NOTAUTH = 9009,
DNS_ERROR_RCODE_NOTZONE = 9010,
DNS_ERROR_RCODE_BADSIG = 9016,
DNS_ERROR_RCODE_BADKEY = 9017,
DNS_ERROR_RCODE_BADTIME = 9018
}

private enum QueryOptions
{
DNS_QUERY_ACCEPT_TRUNCATED_RESPONSE = 1,
DNS_QUERY_BYPASS_CACHE = 8,
DNS_QUERY_DONT_RESET_TTL_VALUES = 0x100000,
DNS_QUERY_NO_HOSTS_FILE = 0x40,
DNS_QUERY_NO_LOCAL_NAME = 0x20,
DNS_QUERY_NO_NETBT = 0x80,
DNS_QUERY_NO_RECURSION = 4,
DNS_QUERY_NO_WIRE_QUERY = 0x10,
DNS_QUERY_RESERVED = -16777216,
DNS_QUERY_RETURN_MESSAGE = 0x200,
DNS_QUERY_STANDARD = 0,
DNS_QUERY_TREAT_AS_FQDN = 0x1000,
DNS_QUERY_USE_TCP_ONLY = 2,
DNS_QUERY_WIRE_ONLY = 0x100
}

public enum QueryTypes
{
DNS_TYPE_A = 1,
DNS_TYPE_CNAME = 5,
DNS_TYPE_MX = 15,
DNS_TYPE_TEXT = 16,
DNS_TYPE_SRV = 33,
DNS_TYPE_PTR = 12

}

[StructLayout(LayoutKind.Explicit)]
private struct DnsRecord
{
[FieldOffset(0)]
public IntPtr pNext;
[FieldOffset(4)]
public string pName;
[FieldOffset(8)]
public short wType;
[FieldOffset(10)]
public short wDataLength;
[FieldOffset(12)]
public uint flags;
[FieldOffset(16)]
public uint dwTtl;
[FieldOffset(20)]
public uint dwReserved;

// below is a partial list of the unionized members for this struct

// for DNS_TYPE_A records
[FieldOffset(24)]
public uint a_IpAddress;

// for DNS_TYPE_ PTR, CNAME, NS, MB, MD, MF, MG, MR records
[FieldOffset(24)]
public IntPtr ptr_pNameHost;

// for DNS_TXT_ DATA, HINFO, ISDN, TXT, X25 records
[FieldOffset(24)]
public uint data_dwStringCount;
[FieldOffset(28)]
public IntPtr data_pStringArray;

// for DNS_TYPE_MX records
[FieldOffset(24)]
public IntPtr mx_pNameExchange;
[FieldOffset(28)]
public short mx_wPreference;
[FieldOffset(30)]
public short mx_Pad;

// for DNS_TYPE_SRV records
[FieldOffset(24)]
public IntPtr srv_pNameTarget;
[FieldOffset(28)]
public short srv_wPriority;
[FieldOffset(30)]
public short srv_wWeight;
[FieldOffset(32)]
public short srv_wPort;
[FieldOffset(34)]
public short srv_Pad;

}

public static string[] GetRecords(string domain, string dnsqtype)
{
IntPtr ptr1 = IntPtr.Zero ;
IntPtr ptr2 = IntPtr.Zero ;
DnsRecord rec;
Dns.QueryTypes qtype = QueryTypes.DNS_TYPE_PTR;
switch(dnsqtype){
case "MX":
qtype = QueryTypes.DNS_TYPE_MX;
break;
case "PTR":
qtype = QueryTypes.DNS_TYPE_PTR;
break;
case "SPF":
qtype = QueryTypes.DNS_TYPE_TEXT;
break;
case "A":
qtype = QueryTypes.DNS_TYPE_A;
break;
}

if(Environment.OSVersion.Platform != PlatformID.Win32NT)
{
throw new NotSupportedException();
}

ArrayList list1 = new ArrayList();
int num1 = DnsQuery(ref domain, qtype, QueryOptions.DNS_QUERY_USE_TCP_ONLY|QueryOptions.DNS_QUERY_BYPASS_CACHE, 0, ref ptr1, 0);
if (num1 != 0)
{
if (num1 == 9003)
{
String[] emErrormessage = new string[1];
emErrormessage.SetValue("No Record Found",0);
return emErrormessage;
}
else
{
String[] emErrormessage = new string[1];
emErrormessage.SetValue("Error During Query Error Number " + num1 , 0);
return emErrormessage;
}
}
for (ptr2 = ptr1; !ptr2.Equals(IntPtr.Zero); ptr2 = rec.pNext)
{
rec = (DnsRecord) Marshal.PtrToStructure(ptr2, typeof(DnsRecord));
if (rec.wType == (short)qtype)
{
string text1 = String.Empty;
switch(qtype)
{
case Dns.QueryTypes.DNS_TYPE_A:
System.Net.IPAddress ip = new System.Net.IPAddress(rec.a_IpAddress);
text1 = ip.ToString();
break;
case Dns.QueryTypes.DNS_TYPE_CNAME:
text1 = Marshal.PtrToStringAuto(rec.ptr_pNameHost);
break;
case Dns.QueryTypes.DNS_TYPE_MX:
text1 = Marshal.PtrToStringAuto(rec.mx_pNameExchange);
string[] mxalookup = PAB.DnsUtils.Dns.GetRecords(Marshal.PtrToStringAuto(rec.mx_pNameExchange), "A");
text1 = text1 + " : " + rec.mx_wPreference.ToString() + " : " ;
foreach (string st in mxalookup)
{
text1 = text1 + st.ToString() + " ";
}

break;
case Dns.QueryTypes.DNS_TYPE_SRV:
text1 = Marshal.PtrToStringAuto(rec.srv_pNameTarget);
break;
case Dns.QueryTypes.DNS_TYPE_PTR:
text1 = Marshal.PtrToStringAuto(rec.ptr_pNameHost);
break;
case Dns.QueryTypes.DNS_TYPE_TEXT:
if (Marshal.PtrToStringAuto(rec.data_pStringArray).ToLower().IndexOf("v=spf") == 0)
{
text1 = Marshal.PtrToStringAuto(rec.data_pStringArray);
}
break;
default:
continue;
}
list1.Add(text1);
}
}

DnsRecordListFree(ptr2, 0);
return (string[]) list1.ToArray(typeof(string));
}
}
}

'@
if ($dnsqtype.ToUpper() -eq "PTR"){
$ipIpaddressSplit = $IpParam.Split(".")
if ($ipIpaddressSplit.length -eq 4){
$revipaddress = $ipIpaddressSplit.GetValue(3) + "." + $ipIpaddressSplit.GetValue(2) + "." + $ipIpaddressSplit.GetValue(1) + "." + $ipIpaddressSplit.GetValue(0) + ".in-addr.arpa"}
else
{"Error in IPaddress Format"
}
}
else {
$revipaddress = $IpParam
}
Compile-Csharp $code
$qrQueryresults = [PAB.DnsUtils.DNS]::GetRecords($revipaddress,$dnsqtype.ToUpper())
""
foreach ($qresult in $qrQueryresults) {
$qresult
}
""

12 comments:

Michael said...

Nice article.

I was looking for the a .Net translated DNS Struct.

Thanks.

Anonymous said...

Thanks. I liked the nice tidy code ;)

Regards,
Farrukh S.
Senior Network Analyst III

Anonymous said...

I really appreciate all your code so I thought I'd contribute.

I think there is a little memory leak here. The DnsRecord list is ptr1 but you are freeing ptr2, which is the last record in the list. Also the second parameter to DnsRecordListFree is the FreeType which should be 1 (DnsFreeRecordList) not 0 (DnsFreeFlat).

So...
DnsRecordListFree(ptr2, 0);
should really be...
DnsRecordListFree(ptr1, 1);

I see that this problem was in the original code from Peter Bromberg.

Thanks again for all the hard work.

Glen said...

Thanks i've updated the download

Cheers
Glen

Anonymous said...

Nice code it works fine... but only in x86 environment. If I try to use it on x64, it fails. Probably because of "FieldOffset"
in DnsRecord struct.

Exception calling "GetRecords" with "2" argument(s): "Could not load type 'DnsRecord' from assembly 'wph_wqww, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 4 that is incorrectly aligned or overlapped by a non-object field."

manudea said...

Tried the script.

Invoking as:
dnsutil MX google.com

I got this error:
Error During Query Error Number 10061

Glen said...

What O/S are you using as far as i've tested this it should work on XP/2003 not sure about vista.

Cheers
Glen

manudea said...

it is vista indeed...

Anonymous said...

ERROR 9561 - WIN7 for qtype DNS_TYPE_PTR

Anonymous said...

nice job, still testing on win7

Anonymous said...

I really appreciate all your code...6 years ! i'm late, very in late !!! A good tutorial for me, mixing powershell and unmanaged code, 32-64 bits.

On XPSP3, I got this error:
"Error During Query Error Number 10061".
I modifie a little and replace "compile-csharp" by the simple cmdlet of PS V2 :
Add-Type -TypeDefinition @"
namespace PAB.DnsUtils
{
...all your code witout lines relatives to "compile"
}
# in the main part of script, i suppress the call to compile-cscharp and it works for me.

On W8-64bits, it also got : "it contains an object field at offset 4 that is incorrectly aligned or overlapped by a non-object field.".
It is certainely because [fieldoffset(x)] has differentes values (x) in 64bits ( sizeof(IntPtr) = 4 in 32bits, sizeof(IntPtr)=8 in 64 bits).

But your script ( vs add-type ) work find in W8-64 if you launche ....\Syswow64\windowspowersell\V1.0\powershell.exe (32 bits over 64 bits).

I don't know how to write a script 32 and 64 with unmanaged code embedded.

Very helpfull for me. Merci.

Noël

Anonymous said...

Sorry, i forgot... i change a parameter in dnsquery call like this :
int num1 = DnsQuery(ref domain, qtype, QueryOptions.DNS_QUERY_STANDARD|QueryOptions.DNS_QUERY_BYPASS_CACHE, 0, ref ptr1, 0);
With "QueryOptions.DNS_QUERY_USE_TCP_ONLY|QueryOptions.DNS_QUERY_BYPASS_CACHE" in the call, i got "error 10061".
Merci encore une fois