Igor Lutsyk
Igor Lutsyk

Reputation: 79

PInvoke DnsQueryEx Async

I'm new to marshaling and will be happy for any advise.
In Windows 8 and Windows Server 2012 we have a new Windows API function: DnsQueryEx
It allows DNS queries to be made asynchronously.

Using C#, I am trying to call it in order to receive MX host records.
As standard, System.Net.DNS is missing support for retrieving MX records with UDP.

I've successfully called DnsQuery API function before using of this example:

http://www.pinvoke.net/default.aspx/dnsapi.DnsQuery

Unfortunately, there are no examples with DnsQueryEx, and i spend some couple of time plaing with possible parameters, but i think i can't find out how I need build DnsAddressArray param.

Why I need to call it asynchronously - because I had developed asynchronous SMTP server, with great socket performance, and I were using DnsQuery function for retrieving MX records, but time to time I am getting DNS error exception there, during call from async context.

Right now, when i am calling DnsQueryEx app process terminates unexpectally, before i were getting error 87 also, which means - wrong parameters.

Also i can't find out how to marshal string to unmanaged code as part of structure?

Here is my code:

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
    }

    private enum QueryTypes
    {
        DNS_TYPE_MX = 15
    }
    [StructLayout(LayoutKind.Sequential)]
    private struct QueryContextStruct
    {
        public int               RefCount;
        public IntPtr                QueryName;
        public short                QueryType;
        public ulong               QueryOptions;
        public DnsQueryResult    QueryResults;
        public CancelHandle    QueryCancelContext;
        public IntPtr              QueryCompletedEvent;
    }
    [StructLayout(LayoutKind.Sequential)]
    private struct DNSQueryRequest
    {
        public uint Version;
        //public string QueryName;
        public QueryTypes QueryType;
        public QueryOptions QueryOptions;
        public IntPtr pDnsServerList;
        public uint InterfaceIndex;
        public IntPtr pQueryCompletionCallback;
        public IntPtr pQueryContext;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct DnsAddr {
        IntPtr  MaxSa;
        IntPtr DnsAddrUserDword;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct DnsAddrArray
    {
        public uint       MaxCount;
        public uint           AddrCount;
        public uint           Tag;
        public ushort            Family;
        public ushort            WordReserved;
        public uint           Flags;
        public uint           MatchFlag;
        public uint           Reserved1;
        public uint           Reserved2;

        public IntPtr AddrArray;
    }

    [StructLayout(LayoutKind.Sequential)]
    private class DnsQueryResult
    {
        public uint Version;
        public int QueryStatus;
        public ulong QueryOptions;
        public IntPtr pQueryRecords;
        public IntPtr reserved;
    }

    [StructLayout(LayoutKind.Sequential)]
    private class CancelHandle
    {
        public byte Handle = 32;
    }
    [DllImport("dnsapi")]
    private static extern int DnsQueryEx(IntPtr pQueryRequest, IntPtr pQueryResults, IntPtr pCancelHandle);

    delegate void DnsReceivedResultDelegate(IntPtr parameter);

    public static void ReceiveMxAsync(string domain)
    {
        // Ensure that this thread's identity is mapped, 1-to-1, with a native OS thread.
        Thread.BeginThreadAffinity();
        GCHandle requestHandle = default(GCHandle);
        GCHandle resultHandle = default(GCHandle);
        GCHandle dnsListHandle = default(GCHandle);
        GCHandle dnsHandle = default(GCHandle);
        GCHandle cancelHandle = default(GCHandle);
        DnsReceivedResultDelegate changeDelegate = ReceivedStatusChangedEvent;
        IntPtr hSCM = IntPtr.Zero;
        IntPtr hService = IntPtr.Zero;
        try
        {
            //request
            DNSQueryRequest queryRequest = new DNSQueryRequest();
            queryRequest.Version = 0x1;
            queryRequest.QueryType = QueryTypes.DNS_TYPE_MX;
            queryRequest.QueryOptions = QueryOptions.DNS_QUERY_BYPASS_CACHE;
            DnsAddr address = new DnsAddr();
            dnsHandle = GCHandle.Alloc(address, GCHandleType.Pinned);
            IntPtr DnsStructure = dnsHandle.AddrOfPinnedObject();

            //getting server list
            DnsAddrArray addressArray = new DnsAddrArray();
            addressArray.MaxCount = 1;
            addressArray.AddrCount = 1;
            addressArray.AddrArray = DnsStructure;
            dnsListHandle = GCHandle.Alloc(addressArray, GCHandleType.Pinned);
            IntPtr pinnedDnsStructure = dnsListHandle.AddrOfPinnedObject();
            queryRequest.pDnsServerList = pinnedDnsStructure;
            queryRequest.InterfaceIndex = 0;
            queryRequest.pQueryCompletionCallback = Marshal.GetFunctionPointerForDelegate(changeDelegate);
            requestHandle = GCHandle.Alloc(queryRequest, GCHandleType.Pinned);
            IntPtr pinnedRequestStructure = requestHandle.AddrOfPinnedObject();

            //result
            DnsQueryResult result = new DnsQueryResult();
            result.Version = 0x1;
            resultHandle = GCHandle.Alloc(result, GCHandleType.Pinned);
            IntPtr pinnedResultStructure = resultHandle.AddrOfPinnedObject();

            //cancel handle
            CancelHandle cancel = new CancelHandle();
            cancelHandle = GCHandle.Alloc(cancel, GCHandleType.Pinned);
            IntPtr pinnedCancleStructure = cancelHandle.AddrOfPinnedObject();

            var res = DnsQueryEx(pinnedRequestStructure, pinnedResultStructure, pinnedCancleStructure);

            //timeout
            //SleepEx(5000, true);
            if (res > 0)
                return;
        }
        catch (Exception ex) 
        {
            if (ex != null)
                return;
        }
        finally
        {
            GC.KeepAlive(changeDelegate);
            if (dnsListHandle != default(GCHandle))
            {
                dnsListHandle.Free();
            }
            if (dnsHandle != default(GCHandle))
            {
                dnsHandle.Free();
            }
            if (requestHandle != default(GCHandle))
            {
                requestHandle.Free();
            }
            if (resultHandle != default(GCHandle))
            {
                resultHandle.Free();
            }
            if (cancelHandle != default(GCHandle))
            {
                cancelHandle.Free();
            }
            Thread.EndThreadAffinity();
        }
    }

    private static void DnsReceivedResultEvent(IntPtr parameter, DnsQueryResult result)
    {

    }
    //==================================================

UPDATE: I have changed code as this:

 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
    }
    private enum QueryTypes
    {
        DNS_TYPE_MX = 15
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct QueryContextStruct
    {
        public int               RefCount;
        public IntPtr                QueryName;
        public short                QueryType;
        public ulong               QueryOptions;
        public DnsQueryResult    QueryResults;
        public CancelHandle    QueryCancelContext;
        public IntPtr              QueryCompletedEvent;
    }
    [StructLayout(LayoutKind.Sequential)]
    private struct DNSQueryRequest
    {
        public uint Version;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string QueryName;
        public QueryTypes QueryType;
        public QueryOptions QueryOptions;
        public IntPtr pDnsServerList;
        public uint InterfaceIndex;
        public IntPtr pQueryCompletionCallback;
        public IntPtr pQueryContext;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct DnsAddr
    {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32500)] //todo: DNS_ADDR_MAX_SOCKADDR_LENGTH
        public byte[] MaxSa;

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        public uint[] DnsAddrUserDword;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct DnsAddrArray
    {
        public uint MaxCount;
        public uint AddrCount;
        public uint Tag;
        public ushort Family;
        public ushort WordReserved;
        public uint Flags;
        public uint MatchFlag;
        public uint Reserved1;
        public uint Reserved2;
        public DnsAddr AddrArray;
    }

    [StructLayout(LayoutKind.Sequential)]
    private class DnsQueryResult
    {
        public uint Version;
        public int QueryStatus;
        public ulong QueryOptions;
        public IntPtr pQueryRecords;
        public IntPtr reserved;
    }

    public unsafe struct CancelHandle
    {
        private fixed byte Reserved[32];
    }

    [DllImport("dnsapi")]
    private static extern int DnsQueryEx(IntPtr pQueryRequest, IntPtr pQueryResults, IntPtr pCancelHandle);

    delegate void DnsReceivedResultDelegate(IntPtr parameter);

    public static void ReceiveMxAsync(string domain)
    {
        // Ensure that this thread's identity is mapped, 1-to-1, with a native OS thread.
        Thread.BeginThreadAffinity();
        GCHandle requestHandle = default(GCHandle);
        GCHandle resultHandle = default(GCHandle);
        GCHandle dnsListHandle = default(GCHandle);
        GCHandle dnsHandle = default(GCHandle);
        GCHandle cancelHandle = default(GCHandle);
        DnsReceivedResultDelegate changeDelegate = ReceivedStatusChangedEvent;
        IntPtr hSCM = IntPtr.Zero;
        IntPtr hService = IntPtr.Zero;
        try
        {
            //request
            DNSQueryRequest queryRequest = new DNSQueryRequest();
            queryRequest.Version = 0x1;
            queryRequest.QueryName = domain;
            queryRequest.QueryType = QueryTypes.DNS_TYPE_MX;
            queryRequest.QueryOptions = QueryOptions.DNS_QUERY_BYPASS_CACHE;
            queryRequest.pDnsServerList = Marshal.AllocHGlobal(Marshal.SizeOf(queryRequest.pDnsServerList));
            queryRequest.InterfaceIndex = 0;
            queryRequest.pQueryCompletionCallback = Marshal.GetFunctionPointerForDelegate(changeDelegate);

            var dnsServerAddrList = new DnsAddrArray();
            dnsServerAddrList.MaxCount = (uint)Marshal.SizeOf(dnsServerAddrList);
            dnsServerAddrList.AddrArray = new DnsAddr();
            Marshal.StructureToPtr(dnsServerAddrList, queryRequest.pDnsServerList, true);

            IntPtr pinnedRequestStructure = Marshal.AllocHGlobal(Marshal.SizeOf(queryRequest));

            //result
            DnsQueryResult result = new DnsQueryResult();
            result.Version = 0x1;
            resultHandle = GCHandle.Alloc(result, GCHandleType.Pinned);
            IntPtr pinnedResultStructure = resultHandle.AddrOfPinnedObject();

            //cancel handle
            CancelHandle cancel = new CancelHandle();
            cancelHandle = GCHandle.Alloc(cancel, GCHandleType.Pinned);
            IntPtr pinnedCancleStructure = cancelHandle.AddrOfPinnedObject();

            //getting server list
            var res = DnsQueryEx(pinnedRequestStructure, pinnedResultStructure, pinnedCancleStructure);

            //timeout
            //SleepEx(5000, true);
            if (res > 0)
                return;
        }
        catch (Exception ex) 
        {
            if (ex != null)
                return;
        }
        finally
        {
            GC.KeepAlive(changeDelegate);
            if (dnsListHandle != default(GCHandle))
            {
                dnsListHandle.Free();
            }
            if (dnsHandle != default(GCHandle))
            {
                dnsHandle.Free();
            }
            if (requestHandle != default(GCHandle))
            {
                requestHandle.Free();
            }
            if (resultHandle != default(GCHandle))
            {
                resultHandle.Free();
            }
            if (cancelHandle != default(GCHandle))
            {
                cancelHandle.Free();
            }
            Thread.EndThreadAffinity();
        }
    }

    private static void DnsReceivedResultEvent(IntPtr parameter, DnsQueryResult result)
    {
        //SUCESS!
    }
    //==================================================

Without luck, still error 87 in response... I think i need to init AddrArray structure with right server's parameters, i would appreciate any help... Here we have C++ working example:

It should be possible to use this as C++ lib, but i would like to avoid one more gateway, and unfortunately i am week in C++...

Upvotes: 2

Views: 1058

Answers (1)

David Heffernan
David Heffernan

Reputation: 613242

tl;dr use C++/CLI to call this exceedingly complex API.


I don't have Windows 8 at hand, so I cannot execute anything. However, I can find a number of errors in the code. I'll list them below. I expect that there will be more errors than I have found, but hopefully this helps.

DNS_QUERY_CANCEL

typedef struct _DNS_QUERY_CANCEL {
  CHAR Reserved[32];
} DNS_QUERY_CANCEL, *PDNS_QUERY_CANCEL;

This is a struct containing an array of char, of length 32. Your translation is incorrect. A literal translation is tricky because you must create types that can be pinned. If you could use an unsafe context then you could use a fixed size buffer.

public unsafe struct DnsQueryCancel
{
    private fixed byte Reserved[32];
}

Otherwise you could just put some fields in to give a struct the right length. That's fine because you do not need to read or write to this struct.

public struct DnsQueryCancel
{
    private long Reserved1;
    private long Reserved2;
    private long Reserved3;
    private long Reserved4;
}

DNS_ADDR_ARRAY

typedef struct _DnsAddrArray {
  DWORD    MaxCount;
  DWORD    AddrCount;
  DWORD    Tag;
  WORD     Family;
  WORD     WordReserved;
  DWORD    Flags;
  DWORD    MatchFlag;
  DWORD    Reserved1;
  DWORD    Reserved2;
  DNS_ADDR AddrArray[ ];
} DNS_ADDR_ARRAY, *PDNS_ADDR_ARRAY;

Your translation is incorrect. The final field is not a pointer. It is an inline array. The struct is therefore variable sized. You cannot declare that in a way that allows it to be pinned. You'd need to do the marshal manually, to unmanaged memory. If you only need to support structs with AddrCount of 1 then you can declare the struct like this:

public struct DnsAddrArray
{
    public uint    MaxCount;
    public uint    AddrCount;
    public uint    Tag;
    public ushort  Family;
    public ushort  WordReserved;
    public uint    Flags;
    public uint    MatchFlag;
    public uint    Reserved1;
    public uint    Reserved2;
    public DnsAddr AddrArray;
}

Note that MaxCount is the size in bytes of the struct. You are setting that incorrectly.

DNS_ADDR

typedef struct _Dns_Addr {
  CHAR  MaxSa[DNS_ADDR_MAX_SOCKADDR_LENGTH];
  DWORD DnsAddrUserDword[8];
} DNS_ADDR, *PDNS_ADDR;

Again, you get this badly wrong in the C#. Assuming the use of fixed this would be:

public unsafe struct DnsAddr
{
    public fixed byte MaxSa[DNS_ADDR_MAX_SOCKADDR_LENGTH];
    public fixed uint DnsAddrUserDword[8];
}

However, fixed buffers are very inconvenient to work with. So I would probably do this as:

public struct DnsAddr
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = DNS_ADDR_MAX_SOCKADDR_LENGTH)]
    public byte[] MaxSa[];

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public uint[] DnsAddrUserDword;
}

Then you'd need to use Marshal.StructureToPtr and Marshal.PtrToStructure for the marshalling. The use of UnmanagedType.ByValArray renders pinning impossible.

Summary

I'm pretty sure that I'm only scratching the surface here. This is a quite horrible interface to attempt to call from C#. I simply would not attempt to do so, and I think I'm reasonably proficient at p/invoke. The smart move here is to use the supplied Windows header files from a mixed mode C++/CLI assembly. I urge you to consider that option.

Upvotes: 1

Related Questions